@configdirector/cli 0.0.0-stage → 0.1.0-beta.1

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.
@@ -0,0 +1,2912 @@
1
+ #!/usr/bin/env node
2
+ import { homedir } from "node:os";
3
+ import { Command, CommanderError } from "commander";
4
+ import { access, mkdir, open, readFile, readdir, stat, writeFile } from "node:fs/promises";
5
+ import * as v from "valibot";
6
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
+ import { createInterface } from "node:readline";
8
+ import ignore from "ignore";
9
+ import picomatch from "picomatch";
10
+ //#region package.json
11
+ var version = "0.1.0-beta.1";
12
+ //#endregion
13
+ //#region src/CliError.ts
14
+ var CliError = class extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = new.target.name;
18
+ }
19
+ };
20
+ //#endregion
21
+ //#region src/ExitCode.ts
22
+ const ExitCode = {
23
+ success: 0,
24
+ problemsFound: 1,
25
+ couldNotRun: 2
26
+ };
27
+ //#endregion
28
+ //#region src/Invocation.ts
29
+ const nonEmptyVariable = (invocation, name) => invocation.environmentVariables[name] || void 0;
30
+ //#endregion
31
+ //#region src/admin-api/admin-api-errors.ts
32
+ const ApiAuthorizationErrorBodySchema = v.object({
33
+ error: v.string(),
34
+ code: v.string()
35
+ });
36
+ const ApiValidationErrorBodySchema = v.object({ errors: v.array(v.object({
37
+ name: v.optional(v.string()),
38
+ message: v.string()
39
+ })) });
40
+ const ServiceSuspendedBodySchema = v.object({ code: v.literal("SERVICE_SUSPENDED") });
41
+ const describeFieldError = (fieldError) => fieldError.name === void 0 ? fieldError.message : `${fieldError.name}: ${fieldError.message}`;
42
+ var ApiAuthorizationError = class extends CliError {
43
+ constructor(code, apiMessage) {
44
+ super(`The Admin API refused the request: ${apiMessage} (${code})`);
45
+ }
46
+ };
47
+ var ApiValidationError = class extends CliError {
48
+ constructor(errors) {
49
+ super(`The Admin API rejected the request:\n${errors.map((error) => ` ${describeFieldError(error)}`).join("\n")}`);
50
+ }
51
+ };
52
+ var ServiceSuspendedError = class extends CliError {
53
+ constructor(retryAfterSeconds) {
54
+ super(`Your organization's service is suspended, so ConfigDirector refuses Admin API requests. The dashboard's billing page shows why and how to reinstate it. Retry after ${retryAfterSeconds} seconds.`);
55
+ }
56
+ };
57
+ var ApiUnreachableError = class extends CliError {
58
+ constructor(baseUrl, cause) {
59
+ const reason = cause instanceof Error && cause.cause instanceof Error ? cause.cause.message : String(cause);
60
+ super(`Could not reach the Admin API at ${baseUrl} (${reason}). Check the network connection, --base-url or CONFIGDIRECTOR_BASE_URL.`);
61
+ }
62
+ };
63
+ var UnexpectedApiResponseError = class extends CliError {
64
+ constructor(url, description) {
65
+ super(`The Admin API answered ${url} with ${description}.`);
66
+ }
67
+ };
68
+ //#endregion
69
+ //#region src/admin-api/AdminApiClient.ts
70
+ const ProjectSchema = v.object({
71
+ id: v.string(),
72
+ name: v.string(),
73
+ slug: v.string()
74
+ });
75
+ const configFields = {
76
+ key: v.string(),
77
+ deprecatedKeys: v.array(v.object({ key: v.string() })),
78
+ client: v.boolean(),
79
+ server: v.boolean()
80
+ };
81
+ const EnumConfigSchema = v.object({
82
+ ...configFields,
83
+ type: v.literal("enum"),
84
+ typeOptions: v.object({
85
+ valueType: v.string(),
86
+ values: v.array(v.string())
87
+ })
88
+ });
89
+ const ConfigSchema = v.variant("type", [EnumConfigSchema, v.object({
90
+ ...configFields,
91
+ type: v.pipe(v.string(), v.notValue("enum"))
92
+ })]);
93
+ const isEnumConfig = (config) => config.type === "enum";
94
+ const readJsonBody = async (response) => {
95
+ try {
96
+ return await response.json();
97
+ } catch {
98
+ return;
99
+ }
100
+ };
101
+ const describeStatus = (response) => `HTTP ${response.status}`;
102
+ const failureFor = (url, response, body) => {
103
+ switch (response.status) {
104
+ case 400: {
105
+ const validationError = v.safeParse(ApiValidationErrorBodySchema, body);
106
+ return validationError.success ? new ApiValidationError(validationError.output.errors) : new UnexpectedApiResponseError(url, describeStatus(response));
107
+ }
108
+ case 403: {
109
+ const authorizationError = v.safeParse(ApiAuthorizationErrorBodySchema, body);
110
+ return authorizationError.success ? new ApiAuthorizationError(authorizationError.output.code, authorizationError.output.error) : new UnexpectedApiResponseError(url, describeStatus(response));
111
+ }
112
+ case 429: {
113
+ const retryAfterSeconds = response.headers.get("retry-after");
114
+ return v.is(ServiceSuspendedBodySchema, body) && retryAfterSeconds !== null ? new ServiceSuspendedError(retryAfterSeconds) : new UnexpectedApiResponseError(url, describeStatus(response));
115
+ }
116
+ default: return new UnexpectedApiResponseError(url, describeStatus(response));
117
+ }
118
+ };
119
+ var AdminApiClient = class {
120
+ constructor(connection) {
121
+ this.connection = connection;
122
+ }
123
+ listProjects() {
124
+ return this.get("/v1/projects", v.array(ProjectSchema));
125
+ }
126
+ listConfigs(projectId) {
127
+ return this.get(`/v1/projects/${encodeURIComponent(projectId)}/configs`, v.array(ConfigSchema));
128
+ }
129
+ async get(path, schema) {
130
+ const url = `${this.connection.baseUrl}${path}`;
131
+ const response = await this.fetchResponse(url);
132
+ const body = await readJsonBody(response);
133
+ if (!response.ok) throw failureFor(url, response, body);
134
+ const parsedBody = v.safeParse(schema, body);
135
+ if (!parsedBody.success) throw new UnexpectedApiResponseError(url, `an ${describeStatus(response)} response the CLI cannot read`);
136
+ return parsedBody.output;
137
+ }
138
+ async fetchResponse(url) {
139
+ try {
140
+ return await fetch(url, { headers: {
141
+ Accept: "application/json",
142
+ Authorization: `Bearer ${this.connection.token}`,
143
+ "User-Agent": `configdirector-cli/${version}`,
144
+ "X-SourceClient": "cli"
145
+ } });
146
+ } catch (error) {
147
+ throw new ApiUnreachableError(this.connection.baseUrl, error);
148
+ }
149
+ }
150
+ };
151
+ //#endregion
152
+ //#region src/file-contents.ts
153
+ const readFileIfPresent = async (file) => {
154
+ try {
155
+ return await readFile(file, "utf8");
156
+ } catch (error) {
157
+ if (error.code === "ENOENT") return;
158
+ throw error;
159
+ }
160
+ };
161
+ const parseJson = (contents) => {
162
+ try {
163
+ return JSON.parse(contents);
164
+ } catch {
165
+ return;
166
+ }
167
+ };
168
+ const pathExists = (path) => stat(path).then(() => true, () => false);
169
+ const fileExists = (path) => stat(path).then((stats) => stats.isFile(), () => false);
170
+ //#endregion
171
+ //#region src/saved-api-token.ts
172
+ const SavedApiTokenSchema = v.object({ token: v.string() });
173
+ const platformConfigDirectory = (invocation) => {
174
+ switch (invocation.platform) {
175
+ case "darwin": return join(invocation.homeDirectory, "Library", "Application Support");
176
+ case "win32": return nonEmptyVariable(invocation, "APPDATA") ?? join(invocation.homeDirectory, "AppData", "Roaming");
177
+ default: return join(invocation.homeDirectory, ".config");
178
+ }
179
+ };
180
+ const userConfigDirectory = (invocation) => nonEmptyVariable(invocation, "XDG_CONFIG_HOME") ?? platformConfigDirectory(invocation);
181
+ const savedApiTokenFile = (invocation) => join(userConfigDirectory(invocation), "configdirector", "config.json");
182
+ const readSavedApiToken = async (invocation) => {
183
+ const file = savedApiTokenFile(invocation);
184
+ const contents = await readFileIfPresent(file);
185
+ if (contents === void 0) return;
186
+ const savedToken = v.safeParse(SavedApiTokenSchema, parseJson(contents));
187
+ if (!savedToken.success) throw new CliError(`${file} does not hold a saved API token. Run \`configdirector login\` again.`);
188
+ return savedToken.output.token;
189
+ };
190
+ const saveApiToken = async (invocation, token) => {
191
+ const file = savedApiTokenFile(invocation);
192
+ await mkdir(dirname(file), {
193
+ recursive: true,
194
+ mode: 448
195
+ });
196
+ const handle = await open(file, "w", 384);
197
+ try {
198
+ await handle.chmod(384);
199
+ await handle.writeFile(`${JSON.stringify({ token }, null, 2)}\n`);
200
+ } finally {
201
+ await handle.close();
202
+ }
203
+ return file;
204
+ };
205
+ //#endregion
206
+ //#region src/configured-api-token.ts
207
+ const configuredApiToken = async (context) => {
208
+ const token = context.tokenOption ?? nonEmptyVariable(context.invocation, "CONFIGDIRECTOR_TOKEN") ?? await readSavedApiToken(context.invocation);
209
+ if (!token) throw new CliError("No API token found. Set CONFIGDIRECTOR_TOKEN, pass --token, or run `configdirector login`.");
210
+ return token;
211
+ };
212
+ //#endregion
213
+ //#region src/project-selection.ts
214
+ const requireProjectSlug = (context) => {
215
+ if (context.projectSlug === void 0) throw new CliError("Name the project with --project <slug> or CONFIGDIRECTOR_PROJECT.");
216
+ return context.projectSlug;
217
+ };
218
+ const requireLinkedProjectSlug = (context, settings) => {
219
+ const slug = context.projectSlug ?? settings?.project;
220
+ if (slug === void 0) throw new CliError("Name the project with --project <slug>, CONFIGDIRECTOR_PROJECT, or `project` in configdirector.json, which `configdirector init` writes.");
221
+ return slug;
222
+ };
223
+ const findProjectBySlug = async (client, slug) => {
224
+ const project = (await client.listProjects()).find((listedProject) => listedProject.slug === slug);
225
+ if (project === void 0) throw new CliError(`No project in the API token's organization has the slug "${slug}".`);
226
+ return project;
227
+ };
228
+ //#endregion
229
+ //#region src/project-settings.ts
230
+ const ProjectSettingsSchema = v.object({
231
+ project: v.optional(v.string()),
232
+ include: v.optional(v.array(v.string())),
233
+ exclude: v.optional(v.array(v.string())),
234
+ ignoreKeys: v.optional(v.array(v.string()))
235
+ });
236
+ const projectSettingsFile = (workingDirectory) => join(workingDirectory, "configdirector.json");
237
+ const describeIssue = (issue) => {
238
+ const path = v.getDotPath(issue);
239
+ return path === null ? issue.message : `${path}: ${issue.message}`;
240
+ };
241
+ const readProjectSettings = async (workingDirectory) => {
242
+ const file = projectSettingsFile(workingDirectory);
243
+ const contents = await readFileIfPresent(file);
244
+ if (contents === void 0) return;
245
+ const settings = v.safeParse(ProjectSettingsSchema, parseJson(contents));
246
+ if (!settings.success) throw new CliError(`${file} is not valid (${settings.issues.map(describeIssue).join("; ")}).`);
247
+ return settings.output;
248
+ };
249
+ //#endregion
250
+ //#region src/commands/init.ts
251
+ const refuseExistingFile = async (file) => {
252
+ if (await access(file).then(() => true, () => false)) throw new CliError(`${file} already exists. init never overwrites it; edit the file instead.`);
253
+ };
254
+ const init = async (context) => {
255
+ const settingsFile = projectSettingsFile(context.invocation.workingDirectory);
256
+ await refuseExistingFile(settingsFile);
257
+ const projectSlug = requireProjectSlug(context);
258
+ const project = await findProjectBySlug(new AdminApiClient({
259
+ baseUrl: context.baseUrl,
260
+ token: await configuredApiToken(context)
261
+ }), projectSlug);
262
+ const settings = {
263
+ project: project.slug,
264
+ include: ["**"],
265
+ exclude: [],
266
+ ignoreKeys: []
267
+ };
268
+ await writeFile(settingsFile, `${JSON.stringify(settings, null, 2)}\n`, { flag: "wx" });
269
+ return {
270
+ json: {
271
+ settingsFile,
272
+ project: {
273
+ id: project.id,
274
+ slug: project.slug,
275
+ name: project.name
276
+ }
277
+ },
278
+ text: `Linked this directory to the project ${project.name} (${project.slug}) in ${settingsFile}\n`,
279
+ exitCode: ExitCode.success
280
+ };
281
+ };
282
+ //#endregion
283
+ //#region src/pasted-api-token.ts
284
+ const Key = {
285
+ carriageReturn: "\r",
286
+ lineFeed: "\n",
287
+ interrupt: "",
288
+ delete: "",
289
+ backspace: "\b"
290
+ };
291
+ const isTerminal = (input) => input.isTTY === true && typeof input.setRawMode === "function";
292
+ const readLine = async (input) => {
293
+ const lines = createInterface({
294
+ input,
295
+ crlfDelay: Infinity
296
+ });
297
+ for await (const line of lines) return line;
298
+ return "";
299
+ };
300
+ const readLineWithoutEcho = (input, output) => new Promise((resolve, reject) => {
301
+ let typedCharacters = "";
302
+ const endHiddenInput = () => {
303
+ input.off("data", onData);
304
+ input.setRawMode(false);
305
+ input.pause();
306
+ output.write("\n");
307
+ };
308
+ const onData = (chunk) => {
309
+ for (const character of chunk.toString()) switch (character) {
310
+ case Key.carriageReturn:
311
+ case Key.lineFeed:
312
+ endHiddenInput();
313
+ resolve(typedCharacters);
314
+ return;
315
+ case Key.interrupt:
316
+ endHiddenInput();
317
+ reject(new CliError("Login canceled."));
318
+ return;
319
+ case Key.delete:
320
+ case Key.backspace:
321
+ typedCharacters = typedCharacters.slice(0, -1);
322
+ break;
323
+ default: typedCharacters += character;
324
+ }
325
+ };
326
+ input.setRawMode(true);
327
+ input.on("data", onData);
328
+ input.resume();
329
+ });
330
+ const readPastedApiToken = async (invocation) => {
331
+ invocation.stderr.write("Paste your API token: ");
332
+ const { stdin } = invocation;
333
+ const token = (isTerminal(stdin) ? await readLineWithoutEcho(stdin, invocation.stderr) : await readLine(stdin)).trim();
334
+ if (token === "") throw new CliError("No API token was pasted.");
335
+ return token;
336
+ };
337
+ //#endregion
338
+ //#region src/commands/login.ts
339
+ const login = async (context) => {
340
+ const token = context.tokenOption ?? await readPastedApiToken(context.invocation);
341
+ await new AdminApiClient({
342
+ baseUrl: context.baseUrl,
343
+ token
344
+ }).listProjects();
345
+ const file = await saveApiToken(context.invocation, token);
346
+ return {
347
+ json: { tokenFile: file },
348
+ text: `Logged in. The API token is saved in ${file}\n`,
349
+ exitCode: ExitCode.success
350
+ };
351
+ };
352
+ //#endregion
353
+ //#region src/plural.ts
354
+ const plural = (count, singular, pluralForm) => count === 1 ? singular : pluralForm;
355
+ //#endregion
356
+ //#region src/scan/source-languages.ts
357
+ const languageOfExtension = {
358
+ ".js": "javascript",
359
+ ".jsx": "javascript",
360
+ ".mjs": "javascript",
361
+ ".cjs": "javascript",
362
+ ".ts": "javascript",
363
+ ".tsx": "javascript",
364
+ ".mts": "javascript",
365
+ ".cts": "javascript",
366
+ ".vue": "javascript",
367
+ ".svelte": "javascript",
368
+ ".dart": "dart",
369
+ ".swift": "swift",
370
+ ".kt": "kotlin",
371
+ ".java": "java",
372
+ ".py": "python",
373
+ ".cs": "csharp",
374
+ ".go": "go"
375
+ };
376
+ const isSourceExtension = (extension) => Object.hasOwn(languageOfExtension, extension);
377
+ const languageSyntax = {
378
+ javascript: {
379
+ lineComment: "//",
380
+ blockComments: true,
381
+ angleBracketGenerics: true
382
+ },
383
+ dart: {
384
+ lineComment: "//",
385
+ blockComments: true,
386
+ angleBracketGenerics: true
387
+ },
388
+ swift: {
389
+ lineComment: "//",
390
+ blockComments: true,
391
+ angleBracketGenerics: true
392
+ },
393
+ kotlin: {
394
+ lineComment: "//",
395
+ blockComments: true,
396
+ angleBracketGenerics: true
397
+ },
398
+ java: {
399
+ lineComment: "//",
400
+ blockComments: true,
401
+ angleBracketGenerics: true
402
+ },
403
+ python: {
404
+ lineComment: "#",
405
+ blockComments: false,
406
+ angleBracketGenerics: false
407
+ },
408
+ csharp: {
409
+ lineComment: "//",
410
+ blockComments: true,
411
+ angleBracketGenerics: true
412
+ },
413
+ go: {
414
+ lineComment: "//",
415
+ blockComments: true,
416
+ angleBracketGenerics: false
417
+ }
418
+ };
419
+ //#endregion
420
+ //#region src/scan/source-code.ts
421
+ const closerOfOpener = {
422
+ "(": ")",
423
+ "[": "]",
424
+ "{": "}"
425
+ };
426
+ const closers = new Set(Object.values(closerOfOpener));
427
+ const quotes = new Set([
428
+ "\"",
429
+ "'",
430
+ "`"
431
+ ]);
432
+ const OPENERS_SEARCHED_FOR_A_CLOSER = 8;
433
+ const isAsciiLetter = (character) => character >= "a" && character <= "z" || character >= "A" && character <= "Z";
434
+ const isDigit = (character) => character >= "0" && character <= "9";
435
+ const isIdentifierStart = (character) => isAsciiLetter(character) || character === "_" || character === "$" || character > "";
436
+ const isIdentifierPart = (character) => isIdentifierStart(character) || isDigit(character);
437
+ const isWhitespace = (character) => character === " " || character === " " || character === "\n" || character === "\r" || character === "\f";
438
+ const endOfLine = (source, start) => {
439
+ const newline = source.indexOf("\n", start);
440
+ return newline === -1 ? source.length : newline;
441
+ };
442
+ const endOfComment = (source, start, syntax) => {
443
+ if (source.startsWith(syntax.lineComment, start)) return endOfLine(source, start);
444
+ if (syntax.blockComments && source.startsWith("/*", start)) {
445
+ const close = source.indexOf("*/", start + 2);
446
+ return close === -1 ? source.length : close + 2;
447
+ }
448
+ };
449
+ const endOfStringLiteral = (source, start) => {
450
+ const quote = source.charAt(start);
451
+ const tripleQuote = quote.repeat(3);
452
+ if (quote !== "`" && source.startsWith(tripleQuote, start)) {
453
+ const close = source.indexOf(tripleQuote, start + 3);
454
+ return close === -1 ? source.length : close + 3;
455
+ }
456
+ let index = start + 1;
457
+ while (index < source.length) {
458
+ const character = source.charAt(index);
459
+ if (character === "\\") index += 2;
460
+ else if (character === quote) return index + 1;
461
+ else if (character === "\n" && quote !== "`") return index;
462
+ else index += 1;
463
+ }
464
+ return source.length;
465
+ };
466
+ const endOfCharactersWhere = (source, start, belongs) => {
467
+ let index = start;
468
+ while (index < source.length && belongs(index)) index += 1;
469
+ return index;
470
+ };
471
+ const endOfNumber = (source, start) => endOfCharactersWhere(source, start, (index) => isIdentifierPart(source.charAt(index)) || source.charAt(index) === "." && isDigit(source.charAt(index + 1)));
472
+ const tokenize = (source, syntax) => {
473
+ const tokens = [];
474
+ const push = (kind, start, end) => tokens.push({
475
+ kind,
476
+ text: source.slice(start, end),
477
+ start,
478
+ end
479
+ });
480
+ let index = 0;
481
+ while (index < source.length) {
482
+ const character = source.charAt(index);
483
+ const commentEnd = endOfComment(source, index, syntax);
484
+ let end;
485
+ if (isWhitespace(character)) end = index + 1;
486
+ else if (commentEnd !== void 0) end = commentEnd;
487
+ else if (quotes.has(character)) {
488
+ end = endOfStringLiteral(source, index);
489
+ push("string", index, end);
490
+ } else if (isIdentifierStart(character)) {
491
+ end = endOfCharactersWhere(source, index, (position) => isIdentifierPart(source.charAt(position)));
492
+ push("identifier", index, end);
493
+ } else if (isDigit(character)) {
494
+ end = endOfNumber(source, index);
495
+ push("number", index, end);
496
+ } else {
497
+ end = index + 1;
498
+ push("punctuation", index, end);
499
+ }
500
+ index = end;
501
+ }
502
+ return tokens;
503
+ };
504
+ const matchedGroups = (tokens) => {
505
+ const closerIndex = new Int32Array(tokens.length).fill(-1);
506
+ const openerIndex = new Int32Array(tokens.length).fill(-1);
507
+ const openOpeners = [];
508
+ tokens.forEach((token, index) => {
509
+ if (token.kind !== "punctuation") return;
510
+ if (Object.hasOwn(closerOfOpener, token.text)) {
511
+ openOpeners.push(index);
512
+ return;
513
+ }
514
+ if (!closers.has(token.text)) return;
515
+ const lowestSearched = Math.max(0, openOpeners.length - OPENERS_SEARCHED_FOR_A_CLOSER);
516
+ for (let position = openOpeners.length - 1; position >= lowestSearched; position -= 1) {
517
+ const opener = openOpeners[position];
518
+ if (closerOfOpener[tokens[opener].text] === token.text) {
519
+ closerIndex[opener] = index;
520
+ openerIndex[index] = opener;
521
+ openOpeners.length = position;
522
+ return;
523
+ }
524
+ }
525
+ });
526
+ return {
527
+ closerIndex,
528
+ openerIndex
529
+ };
530
+ };
531
+ var SourceCode = class {
532
+ tokens;
533
+ syntax;
534
+ groups;
535
+ lineStarts;
536
+ constructor(source, language) {
537
+ this.source = source;
538
+ this.language = language;
539
+ this.syntax = languageSyntax[language];
540
+ this.tokens = tokenize(source, this.syntax);
541
+ this.groups = matchedGroups(this.tokens);
542
+ }
543
+ token(index) {
544
+ return this.tokens[index];
545
+ }
546
+ isPunctuation(index, text) {
547
+ const token = this.tokens[index];
548
+ return token?.kind === "punctuation" && token.text === text;
549
+ }
550
+ isIdentifier(index, text) {
551
+ const token = this.tokens[index];
552
+ return token?.kind === "identifier" && token.text === text;
553
+ }
554
+ openerOfLastToken(range, closer) {
555
+ const opener = this.groups.openerIndex[range.last] ?? -1;
556
+ return this.isPunctuation(range.last, closer) && opener >= range.first ? opener : void 0;
557
+ }
558
+ firstTopLevelIndex(range, isWanted) {
559
+ let index = range.first;
560
+ while (index <= range.last) {
561
+ if (isWanted(index)) return index;
562
+ index = (this.closerOf(index) ?? index) + 1;
563
+ }
564
+ }
565
+ closerOf(openerIndex) {
566
+ const closer = this.groups.closerIndex[openerIndex] ?? -1;
567
+ return closer === -1 ? void 0 : closer;
568
+ }
569
+ textOf(range) {
570
+ const first = this.tokens[range.first];
571
+ const last = this.tokens[range.last];
572
+ return this.source.slice(first.start, last.end);
573
+ }
574
+ commaSeparatedItems(openerIndex) {
575
+ const closer = this.closerOf(openerIndex);
576
+ if (closer === void 0) return;
577
+ const items = [];
578
+ let itemStart = openerIndex + 1;
579
+ let angleBracketDepth = 0;
580
+ let index = openerIndex + 1;
581
+ while (index < closer) {
582
+ const token = this.tokens[index];
583
+ if (token.kind === "punctuation") {
584
+ if (token.text === "," && angleBracketDepth === 0) {
585
+ items.push({
586
+ first: itemStart,
587
+ last: index - 1
588
+ });
589
+ itemStart = index + 1;
590
+ } else if (this.opensTypeArguments(index)) angleBracketDepth += 1;
591
+ else if (token.text === ">" && angleBracketDepth > 0) angleBracketDepth -= 1;
592
+ }
593
+ index = (this.closerOf(index) ?? index) + 1;
594
+ }
595
+ if (itemStart < closer) items.push({
596
+ first: itemStart,
597
+ last: closer - 1
598
+ });
599
+ return items;
600
+ }
601
+ lineOf(tokenIndex) {
602
+ const offset = this.tokens[tokenIndex].start;
603
+ const lineStarts = this.knownLineStarts();
604
+ let low = 0;
605
+ let high = lineStarts.length - 1;
606
+ while (low < high) {
607
+ const middle = Math.ceil((low + high) / 2);
608
+ if (lineStarts[middle] <= offset) low = middle;
609
+ else high = middle - 1;
610
+ }
611
+ return low + 1;
612
+ }
613
+ startsLine(tokenIndex) {
614
+ const token = this.tokens[tokenIndex];
615
+ const previous = this.tokens[tokenIndex - 1];
616
+ return token !== void 0 && (previous === void 0 || this.source.slice(previous.end, token.start).includes("\n"));
617
+ }
618
+ characterAfter(tokenIndex) {
619
+ const token = this.tokens[tokenIndex];
620
+ return token === void 0 ? "" : this.source.charAt(token.end);
621
+ }
622
+ opensTypeArguments(index) {
623
+ const previous = this.tokens[index - 1];
624
+ const current = this.tokens[index];
625
+ return this.syntax.angleBracketGenerics && current?.kind === "punctuation" && current.text === "<" && previous?.kind === "identifier" && previous.end === current.start;
626
+ }
627
+ knownLineStarts() {
628
+ if (this.lineStarts === void 0) {
629
+ const lineStarts = [0];
630
+ for (let index = 0; index < this.source.length; index += 1) if (this.source.charAt(index) === "\n") lineStarts.push(index + 1);
631
+ this.lineStarts = lineStarts;
632
+ }
633
+ return this.lineStarts;
634
+ }
635
+ };
636
+ //#endregion
637
+ //#region src/scan/literals.ts
638
+ const MINIMUM_KEY_LENGTH = 4;
639
+ const MAXIMUM_KEY_LENGTH = 150;
640
+ const NUMBER_LITERAL = /^-?\d[\d_]*(?:\.\d[\d_]*)?(?:[eE][+-]?\d+)?[fFdDmMlL]?$/;
641
+ const FLOAT_MARKERS = [
642
+ ".",
643
+ "e",
644
+ "E",
645
+ "f",
646
+ "F",
647
+ "d",
648
+ "D",
649
+ "m",
650
+ "M"
651
+ ];
652
+ const isAlphanumeric = (character) => isDigit(character) || isAsciiLetter(character);
653
+ const isKeyCharacter = (character) => isAlphanumeric(character) || character === "." || character === "_" || character === "-";
654
+ const fitsKeyGrammar = (text) => text.length >= MINIMUM_KEY_LENGTH && text.length <= MAXIMUM_KEY_LENGTH && isAlphanumeric(text.charAt(0)) && [...text].every(isKeyCharacter);
655
+ const singleToken = (code, range) => range.first === range.last ? code.token(range.first) : void 0;
656
+ const plainStringContent = (code, range) => {
657
+ const token = singleToken(code, range);
658
+ if (token?.kind !== "string" || token.text.length < 2) return;
659
+ const quote = token.text.charAt(0);
660
+ const content = token.text.slice(1, -1);
661
+ return token.text.endsWith(quote) && !content.includes(quote) && !content.includes("\\") && !content.includes("$") ? content : void 0;
662
+ };
663
+ const literalTextOf = (code, expression) => expression.isName ? code.textOf(expression.range) : plainStringContent(code, expression.range);
664
+ const keyLiteralOf = (code, expression) => {
665
+ const content = literalTextOf(code, expression);
666
+ if (content === void 0) return { kind: "notLiteral" };
667
+ return fitsKeyGrammar(content) ? {
668
+ kind: "key",
669
+ key: content
670
+ } : { kind: "outsideKeyGrammar" };
671
+ };
672
+ const isLiteralGroup = (code, range) => (code.isPunctuation(range.first, "{") || code.isPunctuation(range.first, "[")) && code.closerOf(range.first) === range.last;
673
+ const numberLiteralOf = (text) => {
674
+ if (!NUMBER_LITERAL.test(text)) return;
675
+ const value = Number(text.replace(/[_fFdDmMlL]/g, ""));
676
+ if (!Number.isFinite(value)) return;
677
+ return FLOAT_MARKERS.some((marker) => text.includes(marker)) ? {
678
+ type: "float",
679
+ value
680
+ } : {
681
+ type: "integer",
682
+ value
683
+ };
684
+ };
685
+ const inCodeDefaultValueOf = (code, range) => {
686
+ const text = code.textOf(range);
687
+ if (text === "true" || text === "True") return {
688
+ type: "boolean",
689
+ value: true
690
+ };
691
+ if (text === "false" || text === "False") return {
692
+ type: "boolean",
693
+ value: false
694
+ };
695
+ const stringContent = plainStringContent(code, range);
696
+ if (stringContent !== void 0) return {
697
+ type: "string",
698
+ value: stringContent
699
+ };
700
+ if (isLiteralGroup(code, range)) return {
701
+ type: "json",
702
+ value: text
703
+ };
704
+ return numberLiteralOf(text);
705
+ };
706
+ //#endregion
707
+ //#region src/scan/constant-index.ts
708
+ const TOKENS_ENDING_AN_ASSIGNED_LITERAL = new Set([
709
+ ";",
710
+ ",",
711
+ ")",
712
+ "}",
713
+ "]"
714
+ ]);
715
+ const endsAssignedLiteral = (code, stringIndex) => {
716
+ const next = code.token(stringIndex + 1);
717
+ return next === void 0 || next.kind === "punctuation" && TOKENS_ENDING_AN_ASSIGNED_LITERAL.has(next.text) || code.isIdentifier(stringIndex + 1, "as") || code.startsLine(stringIndex + 1);
718
+ };
719
+ const identifierAt = (code, index) => {
720
+ const token = code.token(index);
721
+ return token?.kind === "identifier" ? token.text : void 0;
722
+ };
723
+ const nameAssignedAt = (code, equalsIndex) => {
724
+ const nameOrType = identifierAt(code, equalsIndex - 1);
725
+ if (nameOrType !== void 0) return (code.isPunctuation(equalsIndex - 2, ":") ? identifierAt(code, equalsIndex - 3) : void 0) ?? nameOrType;
726
+ return code.isPunctuation(equalsIndex - 1, ":") && code.characterAfter(equalsIndex - 1) === "=" ? identifierAt(code, equalsIndex - 2) : void 0;
727
+ };
728
+ const nameHoldingLiteralAt = (code, stringIndex) => {
729
+ if (code.isPunctuation(stringIndex - 1, "=")) return nameAssignedAt(code, stringIndex - 1);
730
+ return code.isPunctuation(stringIndex - 1, ":") ? identifierAt(code, stringIndex - 2) : void 0;
731
+ };
732
+ const constantAssignmentsIn = (code) => code.tokens.flatMap((token, index) => {
733
+ if (token.kind !== "string" || !endsAssignedLiteral(code, index)) return [];
734
+ const key = plainStringContent(code, {
735
+ first: index,
736
+ last: index
737
+ });
738
+ if (key === void 0 || !fitsKeyGrammar(key)) return [];
739
+ const name = nameHoldingLiteralAt(code, index);
740
+ return name === void 0 ? [] : [{
741
+ name,
742
+ key,
743
+ tokenIndex: index
744
+ }];
745
+ });
746
+ var ConstantIndex = class {
747
+ keyOfName = /* @__PURE__ */ new Map();
748
+ add(assignment) {
749
+ const indexed = this.keyOfName.get(assignment.name);
750
+ if (indexed === void 0) this.keyOfName.set(assignment.name, {
751
+ kind: "key",
752
+ key: assignment.key
753
+ });
754
+ else if (indexed.kind === "key" && indexed.key !== assignment.key) this.keyOfName.set(assignment.name, { kind: "ambiguous" });
755
+ }
756
+ keyOf(name) {
757
+ const indexed = this.keyOfName.get(name);
758
+ return indexed?.kind === "key" ? indexed.key : void 0;
759
+ }
760
+ };
761
+ //#endregion
762
+ //#region src/scan/known-key-texts.ts
763
+ const knownKeyTextsIn = (code, projectKeys, tokensReadByCalls) => code.tokens.flatMap((token, index) => {
764
+ if (token.kind !== "string" || tokensReadByCalls.has(index)) return [];
765
+ const content = plainStringContent(code, {
766
+ first: index,
767
+ last: index
768
+ });
769
+ return content !== void 0 && projectKeys.has(content) ? [{
770
+ tokenIndex: index,
771
+ key: content
772
+ }] : [];
773
+ });
774
+ //#endregion
775
+ //#region src/scan/nearest-declarations.ts
776
+ var NearestDeclarations = class {
777
+ declarationOfDirectory = /* @__PURE__ */ new Map();
778
+ constructor(highestDirectory, declaredIn) {
779
+ this.highestDirectory = highestDirectory;
780
+ this.declaredIn = declaredIn;
781
+ }
782
+ declarationAbove(file) {
783
+ return this.declarationAtOrAbove(dirname(file));
784
+ }
785
+ declarationAtOrAbove(directory) {
786
+ const declarationLookedUpBefore = this.declarationOfDirectory.get(directory);
787
+ if (declarationLookedUpBefore !== void 0) return declarationLookedUpBefore;
788
+ const parent = dirname(directory);
789
+ const isLastSearched = directory === this.highestDirectory || parent === directory;
790
+ const declaration = this.declaredIn(directory).then((declarationHere) => declarationHere ?? (isLastSearched ? void 0 : this.declarationAtOrAbove(parent)));
791
+ this.declarationOfDirectory.set(directory, declaration);
792
+ return declaration;
793
+ }
794
+ };
795
+ //#endregion
796
+ //#region src/scan/scanned-files.ts
797
+ const EXCLUDED_DIRECTORY_NAMES = new Set([
798
+ ".git",
799
+ "node_modules",
800
+ "vendor",
801
+ "build",
802
+ "dist",
803
+ "target"
804
+ ]);
805
+ const MAXIMUM_FILE_BYTES = 1024 * 1024;
806
+ const BYTES_CHECKED_FOR_BINARY_CONTENT = 8e3;
807
+ const slashSeparated = (path) => path.split(sep).join("/");
808
+ const isInside = (directory, path) => {
809
+ const pathFromDirectory = relative(directory, path);
810
+ return pathFromDirectory !== "" && !pathFromDirectory.startsWith("..") && !isAbsolute(pathFromDirectory);
811
+ };
812
+ const scanRootAt = async (workingDirectory, path) => {
813
+ const rootPath = resolve(workingDirectory, path ?? ".");
814
+ let rootStats;
815
+ try {
816
+ rootStats = await stat(rootPath);
817
+ } catch (error) {
818
+ if (error.code === "ENOENT") throw new CliError(`There is no file or directory at ${path} to scan.`);
819
+ throw error;
820
+ }
821
+ return {
822
+ path: rootPath,
823
+ isDirectory: rootStats.isDirectory(),
824
+ description: path ?? "the current directory"
825
+ };
826
+ };
827
+ const gitignoreRulesIn = async (directory) => {
828
+ const contents = await readFileIfPresent(join(directory, ".gitignore"));
829
+ return contents === void 0 ? [] : [{
830
+ directory,
831
+ rules: ignore().add(contents)
832
+ }];
833
+ };
834
+ const gitRepositoryRootAbove = async (path) => {
835
+ for (let directory = path;; directory = dirname(directory)) {
836
+ if (await pathExists(join(directory, ".git"))) return directory;
837
+ if (dirname(directory) === directory) return;
838
+ }
839
+ };
840
+ const ancestorDirectoriesBelow = (highestDirectory, path) => {
841
+ const ancestors = [];
842
+ for (let directory = dirname(path); isInside(highestDirectory, directory); directory = dirname(directory)) ancestors.unshift(directory);
843
+ return isInside(highestDirectory, path) ? [highestDirectory, ...ancestors] : [];
844
+ };
845
+ const isGitignored = (path, isDirectory, rulesInScope) => {
846
+ for (const { directory, rules } of [...rulesInScope].reverse()) {
847
+ if (!isInside(directory, path)) continue;
848
+ const pathFromDirectory = slashSeparated(relative(directory, path));
849
+ const result = rules.test(isDirectory ? `${pathFromDirectory}/` : pathFromDirectory);
850
+ if (result.ignored || result.unignored) return result.ignored;
851
+ }
852
+ return false;
853
+ };
854
+ const readFileWithinSizeLimit = async (path) => {
855
+ const stats = await stat(path).catch(() => void 0);
856
+ return stats?.isFile() && stats.size <= MAXIMUM_FILE_BYTES ? readFile(path, "utf8") : void 0;
857
+ };
858
+ const hasBinaryContent = (contents) => contents.subarray(0, BYTES_CHECKED_FOR_BINARY_CONTENT).includes(0);
859
+ async function* candidateFiles(directory, rulesInScope) {
860
+ const rulesHere = [...rulesInScope, ...await gitignoreRulesIn(directory)];
861
+ const entries = await readdir(directory, { withFileTypes: true });
862
+ for (const entry of entries.sort((first, second) => first.name < second.name ? -1 : 1)) {
863
+ const path = join(directory, entry.name);
864
+ if (entry.isDirectory()) {
865
+ if (!EXCLUDED_DIRECTORY_NAMES.has(entry.name) && !isGitignored(path, true, rulesHere)) yield* candidateFiles(path, rulesHere);
866
+ } else if (entry.isFile() && !isGitignored(path, false, rulesHere)) yield path;
867
+ }
868
+ }
869
+ async function* scannedFiles(scope) {
870
+ const isIncluded = scope.include === "everyFile" ? () => true : picomatch([...scope.include], { dot: true });
871
+ const isExcluded = picomatch([...scope.exclude], { dot: true });
872
+ const highestGitignoreDirectory = await gitRepositoryRootAbove(scope.root.path) ?? scope.workingDirectory;
873
+ const ancestorRules = (await Promise.all(ancestorDirectoriesBelow(highestGitignoreDirectory, scope.root.path).map(gitignoreRulesIn))).flat();
874
+ const candidates = scope.root.isDirectory ? candidateFiles(scope.root.path, ancestorRules) : (async function* () {
875
+ if (!isGitignored(scope.root.path, false, ancestorRules)) yield scope.root.path;
876
+ })();
877
+ for await (const path of candidates) {
878
+ const extension = extname(path);
879
+ const displayPath = slashSeparated(relative(scope.workingDirectory, path));
880
+ if (!isSourceExtension(extension) || !isIncluded(displayPath) || isExcluded(displayPath)) continue;
881
+ if ((await stat(path)).size > MAXIMUM_FILE_BYTES) continue;
882
+ const contents = await readFile(path);
883
+ if (!hasBinaryContent(contents)) yield {
884
+ path,
885
+ displayPath,
886
+ extension,
887
+ contents: contents.toString("utf8")
888
+ };
889
+ }
890
+ }
891
+ //#endregion
892
+ //#region src/scan/path-aliases.ts
893
+ const WILDCARD = "*";
894
+ const EXACT_MATCH_RANK = Number.POSITIVE_INFINITY;
895
+ const JSON_EXTENSION = ".json";
896
+ const BYTE_ORDER_MARK = "";
897
+ const TYPESCRIPT_CONFIG_FILE = "tsconfig.json";
898
+ const CONFIG_FILE_NAMES = [TYPESCRIPT_CONFIG_FILE, "jsconfig.json"];
899
+ const NO_ALIAS_OPTIONS = {
900
+ paths: void 0,
901
+ baseUrl: void 0
902
+ };
903
+ const TypeScriptConfigSchema = v.object({
904
+ extends: v.optional(v.union([v.string(), v.array(v.string())])),
905
+ compilerOptions: v.optional(v.object({
906
+ baseUrl: v.optional(v.string()),
907
+ paths: v.optional(v.record(v.string(), v.array(v.string())))
908
+ })),
909
+ references: v.optional(v.array(v.object({ path: v.string() })))
910
+ });
911
+ const pathPatternsOf = (targetsOfPattern) => Object.entries(targetsOfPattern).flatMap(([key, targets]) => {
912
+ const wildcard = key.indexOf(WILDCARD);
913
+ if (wildcard === -1) return [{
914
+ kind: "exact",
915
+ key,
916
+ targets
917
+ }];
918
+ return key.includes(WILDCARD, wildcard + 1) ? [] : [{
919
+ kind: "wildcard",
920
+ prefix: key.slice(0, wildcard),
921
+ suffix: key.slice(wildcard + 1),
922
+ targets
923
+ }];
924
+ });
925
+ const aliasMatchOf = (pattern, specifier) => {
926
+ switch (pattern.kind) {
927
+ case "exact": return specifier === pattern.key ? {
928
+ rank: EXACT_MATCH_RANK,
929
+ targets: pattern.targets
930
+ } : void 0;
931
+ case "wildcard": {
932
+ const fits = specifier.length >= pattern.prefix.length + pattern.suffix.length && specifier.startsWith(pattern.prefix) && specifier.endsWith(pattern.suffix);
933
+ const wildcardText = specifier.slice(pattern.prefix.length, specifier.length - pattern.suffix.length);
934
+ return fits ? {
935
+ rank: pattern.prefix.length,
936
+ targets: pattern.targets.map((target) => target.replace(WILDCARD, () => wildcardText))
937
+ } : void 0;
938
+ }
939
+ }
940
+ };
941
+ const bestAliasMatch = (patterns, specifier) => patterns.reduce((best, pattern) => {
942
+ const match = aliasMatchOf(pattern, specifier);
943
+ return match !== void 0 && (best === void 0 || match.rank > best.rank) ? match : best;
944
+ }, void 0);
945
+ const mappedModulePaths = (pathMapping, specifier) => bestAliasMatch(pathMapping.patterns, specifier)?.targets.map((target) => resolve(pathMapping.targetDirectory, target));
946
+ const modulePathsIn = ({ pathMapping, baseUrl }, specifier) => {
947
+ const mappedPaths = pathMapping === void 0 ? void 0 : mappedModulePaths(pathMapping, specifier);
948
+ if (mappedPaths !== void 0) return mappedPaths;
949
+ return baseUrl === void 0 ? [] : [resolve(baseUrl, specifier)];
950
+ };
951
+ const isTrailingComma = (code, index) => code.isPunctuation(index, ",") && (code.isPunctuation(index + 1, "}") || code.isPunctuation(index + 1, "]"));
952
+ const jsonOfConfigFile = (contents) => {
953
+ const code = new SourceCode(contents.startsWith(BYTE_ORDER_MARK) ? contents.slice(1) : contents, "javascript");
954
+ return code.tokens.map((token, index) => {
955
+ return " ".repeat(token.start - (code.token(index - 1)?.end ?? 0)) + (isTrailingComma(code, index) ? " " : token.text);
956
+ }).join("");
957
+ };
958
+ const isRelativeConfigPath = (path) => path.startsWith("./") || path.startsWith("../");
959
+ const extendedConfigFileAt = async (path) => await fileExists(path) || path.endsWith(JSON_EXTENSION) ? path : `${path}${JSON_EXTENSION}`;
960
+ const referencedConfigFileAt = (path) => path.endsWith(JSON_EXTENSION) ? path : join(path, TYPESCRIPT_CONFIG_FILE);
961
+ const typeScriptConfigAt = async (configFile) => {
962
+ const contents = await readFileWithinSizeLimit(configFile);
963
+ const config = v.safeParse(TypeScriptConfigSchema, contents === void 0 ? void 0 : parseJson(jsonOfConfigFile(contents)));
964
+ if (!config.success) return {
965
+ aliasOptions: NO_ALIAS_OPTIONS,
966
+ extendedConfigFiles: [],
967
+ referencedConfigFiles: []
968
+ };
969
+ const { compilerOptions, extends: extendedConfigs, references } = config.output;
970
+ const configDirectory = dirname(configFile);
971
+ const relativeExtendedConfigs = [extendedConfigs ?? []].flat().filter(isRelativeConfigPath);
972
+ return {
973
+ aliasOptions: {
974
+ paths: compilerOptions?.paths === void 0 ? void 0 : {
975
+ targetsOfPattern: compilerOptions.paths,
976
+ configDirectory
977
+ },
978
+ baseUrl: compilerOptions?.baseUrl === void 0 ? void 0 : resolve(configDirectory, compilerOptions.baseUrl)
979
+ },
980
+ extendedConfigFiles: await Promise.all(relativeExtendedConfigs.map((extendedConfig) => extendedConfigFileAt(resolve(configDirectory, extendedConfig)))),
981
+ referencedConfigFiles: (references ?? []).map((reference) => referencedConfigFileAt(resolve(configDirectory, reference.path)))
982
+ };
983
+ };
984
+ const ownOrInherited = (own, inherited) => ({
985
+ paths: own.paths ?? inherited.paths,
986
+ baseUrl: own.baseUrl ?? inherited.baseUrl
987
+ });
988
+ const declaresAliases = (aliasOptions) => aliasOptions.paths !== void 0 || aliasOptions.baseUrl !== void 0;
989
+ const aliasRulesOf = ({ paths, baseUrl }) => ({
990
+ pathMapping: paths === void 0 ? void 0 : {
991
+ patterns: pathPatternsOf(paths.targetsOfPattern),
992
+ targetDirectory: baseUrl ?? paths.configDirectory
993
+ },
994
+ baseUrl
995
+ });
996
+ var TypeScriptConfigs = class {
997
+ configOfFile = /* @__PURE__ */ new Map();
998
+ aliasOptionsOfFile = /* @__PURE__ */ new Map();
999
+ configAt(configFile) {
1000
+ const configReadBefore = this.configOfFile.get(configFile);
1001
+ if (configReadBefore !== void 0) return configReadBefore;
1002
+ const config = typeScriptConfigAt(configFile);
1003
+ this.configOfFile.set(configFile, config);
1004
+ return config;
1005
+ }
1006
+ async aliasOptionsAt(configFile, configFilesExtending = /* @__PURE__ */ new Set()) {
1007
+ const aliasOptionsReadBefore = this.aliasOptionsOfFile.get(configFile);
1008
+ if (aliasOptionsReadBefore !== void 0) return aliasOptionsReadBefore;
1009
+ const config = await this.configAt(configFile);
1010
+ const extendsChain = new Set([...configFilesExtending, configFile]);
1011
+ let inherited = NO_ALIAS_OPTIONS;
1012
+ for (const extendedConfigFile of config.extendedConfigFiles) if (!extendsChain.has(extendedConfigFile)) inherited = ownOrInherited(await this.aliasOptionsAt(extendedConfigFile, extendsChain), inherited);
1013
+ const aliasOptions = ownOrInherited(config.aliasOptions, inherited);
1014
+ this.aliasOptionsOfFile.set(configFile, aliasOptions);
1015
+ return aliasOptions;
1016
+ }
1017
+ };
1018
+ const configFileIn = async (directory) => {
1019
+ for (const configFileName of CONFIG_FILE_NAMES) {
1020
+ const configFile = join(directory, configFileName);
1021
+ if (await fileExists(configFile)) return configFile;
1022
+ }
1023
+ };
1024
+ const aliasRulesDeclaredIn = async (directory) => {
1025
+ const configFile = await configFileIn(directory);
1026
+ if (configFile === void 0) return;
1027
+ const configs = new TypeScriptConfigs();
1028
+ const aliasOptions = await configs.aliasOptionsAt(configFile);
1029
+ if (declaresAliases(aliasOptions)) return [aliasRulesOf(aliasOptions)];
1030
+ const { referencedConfigFiles } = await configs.configAt(configFile);
1031
+ return Promise.all(referencedConfigFiles.map(async (referencedConfigFile) => {
1032
+ const { pathMapping } = aliasRulesOf(await configs.aliasOptionsAt(referencedConfigFile));
1033
+ return {
1034
+ pathMapping,
1035
+ baseUrl: void 0
1036
+ };
1037
+ }));
1038
+ };
1039
+ var PathAliases = class {
1040
+ aliasRules;
1041
+ constructor(highestDirectory) {
1042
+ this.aliasRules = new NearestDeclarations(highestDirectory, aliasRulesDeclaredIn);
1043
+ }
1044
+ async modulePathsFor(specifier, importingFile) {
1045
+ return (await this.aliasRules.declarationAbove(importingFile) ?? []).map((rules) => modulePathsIn(rules, specifier)).find((modulePaths) => modulePaths.length > 0) ?? [];
1046
+ }
1047
+ };
1048
+ //#endregion
1049
+ //#region src/scan/local-imports.ts
1050
+ const JAVASCRIPT_SOURCE_EXTENSIONS = [
1051
+ ".ts",
1052
+ ".tsx",
1053
+ ".js",
1054
+ ".jsx",
1055
+ ".mjs",
1056
+ ".cjs",
1057
+ ".mts",
1058
+ ".cts",
1059
+ ".vue",
1060
+ ".svelte"
1061
+ ];
1062
+ const TYPESCRIPT_EXTENSIONS_FOR = {
1063
+ ".js": [".ts", ".tsx"],
1064
+ ".jsx": [".tsx"],
1065
+ ".mjs": [".mts"],
1066
+ ".cjs": [".cts"]
1067
+ };
1068
+ const PYTHON_PACKAGE_FILE = "__init__.py";
1069
+ const DART_PACKAGE_SCHEME = "package:";
1070
+ const DART_LIBRARY_SCHEME = "dart:";
1071
+ const DART_IMPORT_DIRECTIVES = [
1072
+ "import",
1073
+ "export",
1074
+ "part"
1075
+ ];
1076
+ const DART_RE_EXPORT_DIRECTIVES = ["export"];
1077
+ const SWIFT_PACKAGE_MANIFEST = "Package.swift";
1078
+ const SWIFT_PACKAGE_TARGETS_DIRECTORIES = ["Sources", "Tests"];
1079
+ const XCODE_PROJECT_EXTENSION = ".xcodeproj";
1080
+ const stringAt = (code, index) => code.token(index)?.kind === "string" ? plainStringContent(code, {
1081
+ first: index,
1082
+ last: index
1083
+ }) : void 0;
1084
+ const unquoted = (text) => {
1085
+ const trimmed = text.trim();
1086
+ const quote = trimmed.charAt(0);
1087
+ return (quote === "\"" || quote === "'") && trimmed.endsWith(quote) ? trimmed.slice(1, -1) : trimmed;
1088
+ };
1089
+ const directoriesUpFrom = (directory, highestDirectory) => {
1090
+ const directories = [directory];
1091
+ let current = directory;
1092
+ while (current !== highestDirectory && dirname(current) !== current) {
1093
+ current = dirname(current);
1094
+ directories.push(current);
1095
+ }
1096
+ return directories;
1097
+ };
1098
+ const ancestorDirectory = (directory, levelsUp) => {
1099
+ let ancestor = directory;
1100
+ for (let level = 0; level < levelsUp; level += 1) ancestor = dirname(ancestor);
1101
+ return ancestor;
1102
+ };
1103
+ const isRelativeSpecifier = (specifier) => specifier === "." || specifier === ".." || specifier.startsWith("./") || specifier.startsWith("../");
1104
+ const javascriptSpecifiers = (code) => code.tokens.flatMap((_token, index) => {
1105
+ const specifier = code.isIdentifier(index - 1, "from") || code.isIdentifier(index - 1, "import") || code.isPunctuation(index - 1, "(") && (code.isIdentifier(index - 2, "import") || code.isIdentifier(index - 2, "require")) ? stringAt(code, index) : void 0;
1106
+ return specifier === void 0 ? [] : [specifier];
1107
+ });
1108
+ const indexAfterReExportedNames = (code, exportIndex) => {
1109
+ const namesIndex = exportIndex + 1;
1110
+ if (code.isPunctuation(namesIndex, "{")) {
1111
+ const closer = code.closerOf(namesIndex);
1112
+ return closer === void 0 ? void 0 : closer + 1;
1113
+ }
1114
+ if (code.isPunctuation(namesIndex, "*")) return code.isIdentifier(namesIndex + 1, "as") ? namesIndex + 3 : namesIndex + 1;
1115
+ };
1116
+ const javascriptReExportSpecifiers = (code) => code.tokens.flatMap((_token, index) => {
1117
+ const fromIndex = code.isIdentifier(index, "export") ? indexAfterReExportedNames(code, index) : void 0;
1118
+ const specifier = fromIndex !== void 0 && code.isIdentifier(fromIndex, "from") ? stringAt(code, fromIndex + 1) : void 0;
1119
+ return specifier === void 0 ? [] : [specifier];
1120
+ });
1121
+ const javascriptCandidates = (path) => {
1122
+ const extension = extname(path);
1123
+ const withoutExtension = path.slice(0, path.length - extension.length);
1124
+ return [
1125
+ ...JAVASCRIPT_SOURCE_EXTENSIONS.includes(extension) ? [path] : [],
1126
+ ...(TYPESCRIPT_EXTENSIONS_FOR[extension] ?? []).map((typescriptExtension) => withoutExtension + typescriptExtension),
1127
+ ...JAVASCRIPT_SOURCE_EXTENSIONS.map((sourceExtension) => path + sourceExtension),
1128
+ ...JAVASCRIPT_SOURCE_EXTENSIONS.map((sourceExtension) => join(path, `index${sourceExtension}`))
1129
+ ];
1130
+ };
1131
+ const pythonImportedNames = (code, start) => {
1132
+ const names = [];
1133
+ let isInsideParentheses = false;
1134
+ for (let index = start; index < code.tokens.length; index += 1) {
1135
+ if (index > start && code.startsLine(index) && !isInsideParentheses) break;
1136
+ if (code.isPunctuation(index, "(")) isInsideParentheses = true;
1137
+ else if (code.isPunctuation(index, ")")) break;
1138
+ else if (code.isIdentifier(index, "as")) index += 1;
1139
+ else {
1140
+ const token = code.token(index);
1141
+ if (token?.kind === "identifier") names.push(token.text);
1142
+ }
1143
+ }
1144
+ return names;
1145
+ };
1146
+ const pythonFromImport = (code, fromIndex) => {
1147
+ let index = fromIndex + 1;
1148
+ let level = 0;
1149
+ while (code.isPunctuation(index, ".")) {
1150
+ level += 1;
1151
+ index += 1;
1152
+ }
1153
+ const modulePath = [];
1154
+ for (let token = code.token(index); token?.kind === "identifier" && token.text !== "import"; token = code.token(index)) {
1155
+ modulePath.push(token.text);
1156
+ index += code.isPunctuation(index + 1, ".") ? 2 : 1;
1157
+ }
1158
+ return code.isIdentifier(index, "import") ? {
1159
+ level,
1160
+ modulePath,
1161
+ names: pythonImportedNames(code, index + 1)
1162
+ } : void 0;
1163
+ };
1164
+ const pythonPlainImports = (code, importIndex) => {
1165
+ const imports = [];
1166
+ let modulePath = [];
1167
+ const endModule = () => {
1168
+ if (modulePath.length > 0) imports.push({
1169
+ level: 0,
1170
+ modulePath,
1171
+ names: []
1172
+ });
1173
+ modulePath = [];
1174
+ };
1175
+ for (let index = importIndex + 1; index < code.tokens.length && !code.startsLine(index); index += 1) if (code.isIdentifier(index, "as")) {
1176
+ endModule();
1177
+ index += 1;
1178
+ } else if (code.isPunctuation(index, ",")) endModule();
1179
+ else {
1180
+ const token = code.token(index);
1181
+ if (token?.kind === "identifier") modulePath.push(token.text);
1182
+ }
1183
+ endModule();
1184
+ return imports;
1185
+ };
1186
+ const pythonImports = (code) => code.tokens.flatMap((_token, index) => {
1187
+ if (!code.startsLine(index)) return [];
1188
+ if (code.isIdentifier(index, "from")) {
1189
+ const fromImport = pythonFromImport(code, index);
1190
+ return fromImport === void 0 ? [] : [fromImport];
1191
+ }
1192
+ return code.isIdentifier(index, "import") ? pythonPlainImports(code, index) : [];
1193
+ });
1194
+ const isRelativePythonImport = (pythonImport) => pythonImport.level > 0;
1195
+ const pythonModuleFiles = (base, modulePath) => {
1196
+ if (modulePath.length === 0) return [];
1197
+ const path = join(base, ...modulePath);
1198
+ return [`${path}.py`, join(path, PYTHON_PACKAGE_FILE)];
1199
+ };
1200
+ const dartPackageDeclaredIn = async (directory) => {
1201
+ const nameLine = (await readFileIfPresent(join(directory, "pubspec.yaml")))?.split("\n").find((line) => line.startsWith("name:"));
1202
+ return nameLine === void 0 ? void 0 : {
1203
+ root: directory,
1204
+ name: unquoted(nameLine.slice(5))
1205
+ };
1206
+ };
1207
+ const goModuleDeclaredIn = async (directory) => {
1208
+ const moduleLine = (await readFileIfPresent(join(directory, "go.mod")))?.split("\n").find((line) => line.startsWith("module "));
1209
+ return moduleLine === void 0 ? void 0 : {
1210
+ root: directory,
1211
+ path: unquoted(moduleLine.slice(7))
1212
+ };
1213
+ };
1214
+ const dartSpecifiers = (code, directives) => code.tokens.flatMap((_token, index) => {
1215
+ const specifier = directives.some((directive) => code.isIdentifier(index - 1, directive)) ? stringAt(code, index) : void 0;
1216
+ return specifier === void 0 || specifier.startsWith(DART_LIBRARY_SCHEME) ? [] : [specifier];
1217
+ });
1218
+ const goImportPaths = (code) => code.tokens.flatMap((_token, index) => {
1219
+ if (!code.isIdentifier(index, "import")) return [];
1220
+ const lastIndex = (code.isPunctuation(index + 1, "(") ? code.closerOf(index + 1) : void 0) ?? index + 1;
1221
+ const paths = [];
1222
+ for (let pathIndex = index + 1; pathIndex <= lastIndex; pathIndex += 1) {
1223
+ const path = stringAt(code, pathIndex);
1224
+ if (path !== void 0) paths.push(path);
1225
+ }
1226
+ return paths;
1227
+ });
1228
+ const swiftProjectDeclaredIn = async (directory) => {
1229
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
1230
+ if (entries.some((entry) => entry.isFile() && entry.name === SWIFT_PACKAGE_MANIFEST)) return {
1231
+ kind: "swiftPackage",
1232
+ root: directory
1233
+ };
1234
+ return entries.some((entry) => entry.isDirectory() && extname(entry.name) === XCODE_PROJECT_EXTENSION) ? {
1235
+ kind: "xcodeProject",
1236
+ root: directory
1237
+ } : void 0;
1238
+ };
1239
+ const swiftPackageTargetOf = (packageRoot, directory) => {
1240
+ const [targetsDirectory, targetName] = relative(packageRoot, directory).split(sep);
1241
+ if (targetsDirectory === void 0 || !SWIFT_PACKAGE_TARGETS_DIRECTORIES.includes(targetsDirectory)) return packageRoot;
1242
+ return targetName === void 0 ? join(packageRoot, targetsDirectory) : join(packageRoot, targetsDirectory, targetName);
1243
+ };
1244
+ const goPackageNameIn = (code) => {
1245
+ const name = code.token(1);
1246
+ return code.isIdentifier(0, "package") && name?.kind === "identifier" ? name.text : void 0;
1247
+ };
1248
+ const goPackageNameOfFile = async (file) => {
1249
+ const contents = await readFileWithinSizeLimit(file);
1250
+ return contents === void 0 ? void 0 : goPackageNameIn(new SourceCode(contents, "go"));
1251
+ };
1252
+ var LocalImports = class {
1253
+ dartPackages;
1254
+ goModules;
1255
+ swiftProjects;
1256
+ pathAliases;
1257
+ goFilesOfDirectory = /* @__PURE__ */ new Map();
1258
+ fileExistence = /* @__PURE__ */ new Map();
1259
+ constructor(highestDirectory) {
1260
+ this.highestDirectory = highestDirectory;
1261
+ this.dartPackages = new NearestDeclarations(highestDirectory, dartPackageDeclaredIn);
1262
+ this.goModules = new NearestDeclarations(highestDirectory, goModuleDeclaredIn);
1263
+ this.swiftProjects = new NearestDeclarations(highestDirectory, swiftProjectDeclaredIn);
1264
+ this.pathAliases = new PathAliases(highestDirectory);
1265
+ }
1266
+ filesImportedBy(code, file) {
1267
+ switch (code.language) {
1268
+ case "javascript": return this.javascriptFilesAt(javascriptSpecifiers(code), file);
1269
+ case "python": return this.pythonFilesAt(pythonImports(code), file);
1270
+ case "dart": return this.dartFilesAt(dartSpecifiers(code, DART_IMPORT_DIRECTIVES), file);
1271
+ case "go": return this.goFilesImportedBy(code, file);
1272
+ case "swift":
1273
+ case "kotlin":
1274
+ case "java":
1275
+ case "csharp": return Promise.resolve([]);
1276
+ }
1277
+ }
1278
+ filesOfSamePackageOrTarget(code, file) {
1279
+ switch (code.language) {
1280
+ case "go": return this.goFilesOfSamePackage(code, file);
1281
+ case "swift": return this.swiftFilesOfSameTarget(file);
1282
+ case "javascript":
1283
+ case "python":
1284
+ case "dart":
1285
+ case "kotlin":
1286
+ case "java":
1287
+ case "csharp": return Promise.resolve([]);
1288
+ }
1289
+ }
1290
+ filesReExportedBy(code, file) {
1291
+ switch (code.language) {
1292
+ case "javascript": return this.javascriptFilesAt(javascriptReExportSpecifiers(code), file);
1293
+ case "python": return basename(file) === PYTHON_PACKAGE_FILE ? this.pythonFilesAt(pythonImports(code).filter(isRelativePythonImport), file) : Promise.resolve([]);
1294
+ case "dart": return this.dartFilesAt(dartSpecifiers(code, DART_RE_EXPORT_DIRECTIVES), file);
1295
+ case "go":
1296
+ case "swift":
1297
+ case "kotlin":
1298
+ case "java":
1299
+ case "csharp": return Promise.resolve([]);
1300
+ }
1301
+ }
1302
+ fileExistsAt(path) {
1303
+ const existenceCheckedBefore = this.fileExistence.get(path);
1304
+ if (existenceCheckedBefore !== void 0) return existenceCheckedBefore;
1305
+ const existence = fileExists(path);
1306
+ this.fileExistence.set(path, existence);
1307
+ return existence;
1308
+ }
1309
+ async firstExistingFile(candidates) {
1310
+ for (const candidate of candidates) if (await this.fileExistsAt(candidate)) return candidate;
1311
+ }
1312
+ async existingFiles(candidates) {
1313
+ const existing = [];
1314
+ for (const candidate of candidates) if (await this.fileExistsAt(candidate)) existing.push(candidate);
1315
+ return existing;
1316
+ }
1317
+ async javascriptFilesAt(specifiers, file) {
1318
+ const files = [];
1319
+ for (const specifier of specifiers) {
1320
+ const modulePaths = isRelativeSpecifier(specifier) ? [join(dirname(file), specifier)] : await this.pathAliases.modulePathsFor(specifier, file);
1321
+ const importedFile = await this.firstExistingFile(modulePaths.flatMap(javascriptCandidates));
1322
+ if (importedFile !== void 0) files.push(importedFile);
1323
+ }
1324
+ return files;
1325
+ }
1326
+ async pythonFilesAt(imports, file) {
1327
+ const files = [];
1328
+ for (const { level, modulePath, names } of imports) {
1329
+ const bases = level === 0 ? directoriesUpFrom(dirname(file), this.highestDirectory) : [ancestorDirectory(dirname(file), level - 1)];
1330
+ for (const base of bases) {
1331
+ const moduleFiles = await this.existingFiles([...pythonModuleFiles(base, modulePath), ...names.flatMap((name) => pythonModuleFiles(base, [...modulePath, name]))]);
1332
+ if (moduleFiles.length > 0) {
1333
+ files.push(...moduleFiles);
1334
+ break;
1335
+ }
1336
+ }
1337
+ }
1338
+ return files;
1339
+ }
1340
+ async dartFilesAt(specifiers, file) {
1341
+ const ownPackage = await this.dartPackages.declarationAbove(file);
1342
+ const candidates = specifiers.flatMap((specifier) => {
1343
+ if (!specifier.startsWith(DART_PACKAGE_SCHEME)) return [join(dirname(file), specifier)];
1344
+ const [packageName, ...pathInPackage] = specifier.slice(8).split("/");
1345
+ return ownPackage !== void 0 && packageName === ownPackage.name ? [join(ownPackage.root, "lib", ...pathInPackage)] : [];
1346
+ });
1347
+ return this.existingFiles(candidates);
1348
+ }
1349
+ async goFilesImportedBy(code, file) {
1350
+ const module = await this.goModules.declarationAbove(file);
1351
+ if (module === void 0) return [];
1352
+ const packageDirectories = goImportPaths(code).filter((path) => path === module.path || path.startsWith(`${module.path}/`)).map((path) => join(module.root, path.slice(module.path.length)));
1353
+ return (await Promise.all(packageDirectories.map((directory) => this.goFilesIn(directory)))).flat();
1354
+ }
1355
+ async goFilesOfSamePackage(code, file) {
1356
+ const packageName = goPackageNameIn(code);
1357
+ if (packageName === void 0) return [];
1358
+ const files = await this.goFilesIn(dirname(file));
1359
+ const packageNames = await Promise.all(files.map(goPackageNameOfFile));
1360
+ return files.filter((_file, index) => packageNames[index] === packageName);
1361
+ }
1362
+ async swiftFilesOfSameTarget(file) {
1363
+ const target = await this.swiftTargetOfDirectory(dirname(file));
1364
+ return this.swiftFilesOfTargetIn(target, target);
1365
+ }
1366
+ async swiftFilesOfTargetIn(target, directory) {
1367
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
1368
+ const files = entries.filter((entry) => entry.isFile() && extname(entry.name) === ".swift").map((entry) => join(directory, entry.name));
1369
+ const filesOfSubdirectories = await Promise.all(entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
1370
+ const subdirectory = join(directory, entry.name);
1371
+ return await this.swiftTargetOfDirectory(subdirectory) === target ? this.swiftFilesOfTargetIn(target, subdirectory) : [];
1372
+ }));
1373
+ return [...files, ...filesOfSubdirectories.flat()];
1374
+ }
1375
+ async swiftTargetOfDirectory(directory) {
1376
+ const project = await this.swiftProjects.declarationAtOrAbove(directory);
1377
+ if (project === void 0) return directory;
1378
+ switch (project.kind) {
1379
+ case "swiftPackage": return swiftPackageTargetOf(project.root, directory);
1380
+ case "xcodeProject": return project.root;
1381
+ }
1382
+ }
1383
+ goFilesIn(directory) {
1384
+ const filesListedBefore = this.goFilesOfDirectory.get(directory);
1385
+ if (filesListedBefore !== void 0) return filesListedBefore;
1386
+ const files = readdir(directory, { withFileTypes: true }).then((entries) => entries.filter((entry) => entry.isFile() && extname(entry.name) === ".go").map((entry) => join(directory, entry.name)), () => []);
1387
+ this.goFilesOfDirectory.set(directory, files);
1388
+ return files;
1389
+ }
1390
+ };
1391
+ //#endregion
1392
+ //#region src/scan/project-areas.ts
1393
+ const GRADLE_BUILD_FILES = ["build.gradle.kts", "build.gradle"];
1394
+ const MAVEN_BUILD_FILE = "pom.xml";
1395
+ const ANDROID_MODULE_MARKERS = [
1396
+ "com.android.",
1397
+ "android {",
1398
+ "android{"
1399
+ ];
1400
+ const NUXT_CONFIG_FILES = [
1401
+ "nuxt.config.ts",
1402
+ "nuxt.config.js",
1403
+ "nuxt.config.mjs",
1404
+ "nuxt.config.mts"
1405
+ ];
1406
+ const NUXT_SERVER_DIRECTORY = "server";
1407
+ const buildModuleDeclaredIn = async (directory) => {
1408
+ for (const buildFile of GRADLE_BUILD_FILES) {
1409
+ const contents = await readFileIfPresent(join(directory, buildFile));
1410
+ if (contents !== void 0) return ANDROID_MODULE_MARKERS.some((marker) => contents.includes(marker)) ? "android" : "jvm";
1411
+ }
1412
+ return await readFileIfPresent(join(directory, MAVEN_BUILD_FILE)) === void 0 ? void 0 : "jvm";
1413
+ };
1414
+ const nuxtAppRootDeclaredIn = async (directory) => {
1415
+ for (const configFile of NUXT_CONFIG_FILES) if (await fileExists(join(directory, configFile))) return directory;
1416
+ };
1417
+ var ProjectAreas = class {
1418
+ buildModules;
1419
+ nuxtAppRoots;
1420
+ constructor(highestDirectory) {
1421
+ this.buildModules = new NearestDeclarations(highestDirectory, buildModuleDeclaredIn);
1422
+ this.nuxtAppRoots = new NearestDeclarations(highestDirectory, nuxtAppRootDeclaredIn);
1423
+ }
1424
+ async contain(area, file) {
1425
+ switch (area) {
1426
+ case "android-module": return await this.buildModuleOf(file) === "android";
1427
+ case "jvm-module": return await this.buildModuleOf(file) === "jvm";
1428
+ case "nuxt-app": return !await this.isInNuxtServerDirectory(file);
1429
+ case "nuxt-server": return this.isInNuxtServerDirectory(file);
1430
+ }
1431
+ }
1432
+ async buildModuleOf(file) {
1433
+ return await this.buildModules.declarationAbove(file) ?? "jvm";
1434
+ }
1435
+ async isInNuxtServerDirectory(file) {
1436
+ const appRoot = await this.nuxtAppRoots.declarationAbove(file);
1437
+ return appRoot !== void 0 && relative(appRoot, file).split(sep)[0] === NUXT_SERVER_DIRECTORY;
1438
+ }
1439
+ };
1440
+ //#endregion
1441
+ //#region src/scan/sdk-families.ts
1442
+ const sdkTypes = ["client", "server"];
1443
+ const javascriptExtensions = [
1444
+ ".js",
1445
+ ".jsx",
1446
+ ".mjs",
1447
+ ".cjs",
1448
+ ".ts",
1449
+ ".tsx",
1450
+ ".mts",
1451
+ ".cts"
1452
+ ];
1453
+ const browserExtensions = [
1454
+ ...javascriptExtensions,
1455
+ ".vue",
1456
+ ".svelte"
1457
+ ];
1458
+ const vueExtensions = [...javascriptExtensions, ".vue"];
1459
+ const read = (name, options = {}) => ({
1460
+ name,
1461
+ form: "method",
1462
+ argumentCount: options.argumentCount,
1463
+ keyArgument: {
1464
+ kind: "key",
1465
+ slot: options.key ?? { position: 0 }
1466
+ },
1467
+ inCodeDefault: options.inCodeDefault ?? { position: 1 },
1468
+ getterType: options.getterType,
1469
+ test: false
1470
+ });
1471
+ const readFunction = (name, options = {}) => ({
1472
+ ...read(name, options),
1473
+ form: "function"
1474
+ });
1475
+ const typedReads = (namesByGetterType, options = {}) => Object.entries(namesByGetterType).flatMap(([getterType, names]) => names.map((name) => read(name, {
1476
+ ...options,
1477
+ getterType
1478
+ })));
1479
+ const keyOnly = (name, key = { position: 0 }) => ({
1480
+ name,
1481
+ form: "method",
1482
+ keyArgument: {
1483
+ kind: "key",
1484
+ slot: key
1485
+ },
1486
+ test: false
1487
+ });
1488
+ const testMethod = (name, key = { position: 0 }) => ({
1489
+ ...keyOnly(name, key),
1490
+ test: true
1491
+ });
1492
+ const testMap = (name, form, slot = { position: 0 }, property) => ({
1493
+ name,
1494
+ form,
1495
+ keyArgument: {
1496
+ kind: "mapKeys",
1497
+ slot,
1498
+ property
1499
+ },
1500
+ test: true
1501
+ });
1502
+ const testAnnotation = (name) => ({
1503
+ name,
1504
+ form: "annotation",
1505
+ keyArgument: {
1506
+ kind: "key",
1507
+ slot: { label: "key" }
1508
+ },
1509
+ test: true
1510
+ });
1511
+ const javascriptClientCalls = [
1512
+ read("getValue"),
1513
+ read("watch"),
1514
+ keyOnly("unwatch")
1515
+ ];
1516
+ const javascriptTestClientCalls = [
1517
+ testMap("createTestClient", "function", { position: 0 }, "values"),
1518
+ testMethod("setValue"),
1519
+ testMethod("removeValue"),
1520
+ testMap("replaceValues", "method")
1521
+ ];
1522
+ const openFeatureJavascriptGetters = typedReads({
1523
+ boolean: ["getBooleanValue", "getBooleanDetails"],
1524
+ string: ["getStringValue", "getStringDetails"],
1525
+ number: ["getNumberValue", "getNumberDetails"],
1526
+ json: ["getObjectValue", "getObjectDetails"]
1527
+ });
1528
+ const pythonLabels = {
1529
+ key: {
1530
+ position: 0,
1531
+ label: "config_key"
1532
+ },
1533
+ inCodeDefault: {
1534
+ position: 1,
1535
+ label: "default"
1536
+ }
1537
+ };
1538
+ const openFeaturePythonLabels = {
1539
+ key: {
1540
+ position: 0,
1541
+ label: "flag_key"
1542
+ },
1543
+ inCodeDefault: {
1544
+ position: 1,
1545
+ label: "default_value"
1546
+ }
1547
+ };
1548
+ const csharpLabels = {
1549
+ key: {
1550
+ position: 0,
1551
+ label: "configKey"
1552
+ },
1553
+ inCodeDefault: {
1554
+ position: 1,
1555
+ label: "defaultValue"
1556
+ }
1557
+ };
1558
+ const openFeatureCsharpLabels = {
1559
+ key: {
1560
+ position: 0,
1561
+ label: "flagKey"
1562
+ },
1563
+ inCodeDefault: {
1564
+ position: 1,
1565
+ label: "defaultValue"
1566
+ }
1567
+ };
1568
+ const openFeatureGoArguments = {
1569
+ key: { position: 1 },
1570
+ inCodeDefault: { position: 2 },
1571
+ argumentCount: { minimum: 4 }
1572
+ };
1573
+ const kotlinLabels = {
1574
+ key: {
1575
+ position: 0,
1576
+ label: "key"
1577
+ },
1578
+ inCodeDefault: {
1579
+ position: 1,
1580
+ label: "defaultValue"
1581
+ }
1582
+ };
1583
+ const swiftLabels = {
1584
+ key: { label: "for" },
1585
+ inCodeDefault: { label: "default" }
1586
+ };
1587
+ const openFeatureSwiftLabels = {
1588
+ key: { label: "key" },
1589
+ inCodeDefault: { label: "defaultValue" }
1590
+ };
1591
+ const sdkFamilies = {
1592
+ "js-client": {
1593
+ sdkType: "client",
1594
+ extensions: browserExtensions,
1595
+ importMarkers: ["@configdirector/client-sdk"],
1596
+ calls: [...javascriptClientCalls, ...javascriptTestClientCalls]
1597
+ },
1598
+ "js-server": {
1599
+ sdkType: "server",
1600
+ extensions: javascriptExtensions,
1601
+ importMarkers: ["@configdirector/server-sdk"],
1602
+ calls: [...javascriptClientCalls, ...javascriptTestClientCalls]
1603
+ },
1604
+ react: {
1605
+ sdkType: "client",
1606
+ extensions: javascriptExtensions,
1607
+ importMarkers: ["@configdirector/react-web-sdk"],
1608
+ calls: [
1609
+ readFunction("useConfigValue"),
1610
+ ...javascriptClientCalls,
1611
+ ...javascriptTestClientCalls
1612
+ ]
1613
+ },
1614
+ "react-native": {
1615
+ sdkType: "client",
1616
+ extensions: javascriptExtensions,
1617
+ importMarkers: ["@configdirector/react-native-sdk"],
1618
+ calls: [
1619
+ readFunction("useConfigValue"),
1620
+ ...javascriptClientCalls,
1621
+ ...javascriptTestClientCalls
1622
+ ]
1623
+ },
1624
+ vue: {
1625
+ sdkType: "client",
1626
+ extensions: vueExtensions,
1627
+ importMarkers: ["@configdirector/vue-sdk"],
1628
+ calls: [
1629
+ readFunction("useConfigValue"),
1630
+ ...javascriptClientCalls,
1631
+ ...javascriptTestClientCalls
1632
+ ]
1633
+ },
1634
+ "nuxt-client": {
1635
+ sdkType: "client",
1636
+ extensions: vueExtensions,
1637
+ importMarkers: [
1638
+ "useConfigDirectorValue",
1639
+ "useConfigDirectorClient",
1640
+ "@configdirector/nuxt-sdk"
1641
+ ],
1642
+ projectArea: "nuxt-app",
1643
+ calls: [
1644
+ readFunction("useConfigDirectorValue"),
1645
+ ...javascriptClientCalls,
1646
+ ...javascriptTestClientCalls
1647
+ ]
1648
+ },
1649
+ "nuxt-server": {
1650
+ sdkType: "server",
1651
+ extensions: javascriptExtensions,
1652
+ importMarkers: ["useConfigDirectorClient"],
1653
+ projectArea: "nuxt-server",
1654
+ calls: javascriptClientCalls
1655
+ },
1656
+ "nextjs-client": {
1657
+ sdkType: "client",
1658
+ extensions: javascriptExtensions,
1659
+ importMarkers: ["@configdirector/nextjs-sdk/client"],
1660
+ calls: [
1661
+ readFunction("useConfigValue"),
1662
+ ...javascriptClientCalls,
1663
+ ...javascriptTestClientCalls
1664
+ ]
1665
+ },
1666
+ "nextjs-server": {
1667
+ sdkType: "server",
1668
+ extensions: javascriptExtensions,
1669
+ importMarkers: ["@configdirector/nextjs-sdk/server"],
1670
+ calls: [...javascriptClientCalls, ...javascriptTestClientCalls]
1671
+ },
1672
+ "openfeature-web": {
1673
+ sdkType: "client",
1674
+ extensions: browserExtensions,
1675
+ importMarkers: ["@openfeature/web-sdk"],
1676
+ calls: openFeatureJavascriptGetters
1677
+ },
1678
+ "openfeature-node": {
1679
+ sdkType: "server",
1680
+ extensions: javascriptExtensions,
1681
+ importMarkers: ["@openfeature/server-sdk"],
1682
+ calls: openFeatureJavascriptGetters
1683
+ },
1684
+ flutter: {
1685
+ sdkType: "client",
1686
+ extensions: [".dart"],
1687
+ importMarkers: ["package:configdirector_flutter_client_sdk/"],
1688
+ calls: [
1689
+ read("getValue"),
1690
+ read("evaluate"),
1691
+ read("watch"),
1692
+ keyOnly("unwatch"),
1693
+ testMap("createTestClient", "function", { label: "values" }),
1694
+ testMethod("setValue"),
1695
+ testMethod("removeValue"),
1696
+ testMap("replaceValues", "method")
1697
+ ]
1698
+ },
1699
+ "openfeature-flutter": {
1700
+ sdkType: "client",
1701
+ extensions: [".dart"],
1702
+ importMarkers: ["package:openfeature_dart_client_sdk/"],
1703
+ calls: typedReads({
1704
+ boolean: ["getBooleanValue", "getBooleanDetails"],
1705
+ string: ["getStringValue", "getStringDetails"],
1706
+ integer: ["getIntegerValue", "getIntegerDetails"],
1707
+ float: ["getDoubleValue", "getDoubleDetails"],
1708
+ json: ["getStructureValue", "getStructureDetails"]
1709
+ })
1710
+ },
1711
+ swift: {
1712
+ sdkType: "client",
1713
+ extensions: [".swift"],
1714
+ importMarkers: ["import ConfigDirector"],
1715
+ providerPackagesContainingMarkers: ["ConfigDirectorOpenFeatureProvider"],
1716
+ calls: [
1717
+ read("value", swiftLabels),
1718
+ read("values", swiftLabels),
1719
+ testMap("makeTestClient", "function", { label: "values" }),
1720
+ testMethod("setValue", { label: "for" }),
1721
+ testMethod("removeValue", { label: "for" }),
1722
+ testMap("replaceValues", "method")
1723
+ ]
1724
+ },
1725
+ "openfeature-swift": {
1726
+ sdkType: "client",
1727
+ extensions: [".swift"],
1728
+ importMarkers: ["import OpenFeature"],
1729
+ calls: [
1730
+ ...typedReads({
1731
+ boolean: ["getBooleanValue", "getBooleanDetails"],
1732
+ string: ["getStringValue", "getStringDetails"],
1733
+ integer: ["getIntegerValue", "getIntegerDetails"],
1734
+ float: ["getDoubleValue", "getDoubleDetails"],
1735
+ json: ["getObjectValue", "getObjectDetails"]
1736
+ }, openFeatureSwiftLabels),
1737
+ read("getValue", openFeatureSwiftLabels),
1738
+ read("getDetails", openFeatureSwiftLabels)
1739
+ ]
1740
+ },
1741
+ android: {
1742
+ sdkType: "client",
1743
+ extensions: [".kt", ".java"],
1744
+ importMarkers: ["com.configdirector"],
1745
+ providerPackagesContainingMarkers: ["com.configdirector.openfeature"],
1746
+ projectArea: "android-module",
1747
+ calls: [
1748
+ read("value", kotlinLabels),
1749
+ read("values", kotlinLabels),
1750
+ read("evaluate", kotlinLabels),
1751
+ ...typedReads({
1752
+ boolean: [
1753
+ "getBoolean",
1754
+ "evaluateBoolean",
1755
+ "watchBoolean"
1756
+ ],
1757
+ string: [
1758
+ "getString",
1759
+ "evaluateString",
1760
+ "watchString"
1761
+ ],
1762
+ integer: [
1763
+ "getInt",
1764
+ "evaluateInt",
1765
+ "watchInt"
1766
+ ],
1767
+ float: [
1768
+ "getDouble",
1769
+ "evaluateDouble",
1770
+ "watchDouble"
1771
+ ],
1772
+ json: [
1773
+ "getJsonObject",
1774
+ "getJsonArray",
1775
+ "evaluateJsonObject",
1776
+ "evaluateJsonArray",
1777
+ "watchJsonObject",
1778
+ "watchJsonArray"
1779
+ ]
1780
+ }, kotlinLabels),
1781
+ readFunction("configValue", kotlinLabels),
1782
+ testMap("createTestClient", "function", {
1783
+ position: 0,
1784
+ label: "values"
1785
+ }),
1786
+ testMethod("setValue", {
1787
+ position: 0,
1788
+ label: "key"
1789
+ }),
1790
+ testMethod("removeValue", {
1791
+ position: 0,
1792
+ label: "key"
1793
+ }),
1794
+ testMap("replaceValues", "method")
1795
+ ]
1796
+ },
1797
+ "openfeature-android": {
1798
+ sdkType: "client",
1799
+ extensions: [".kt", ".java"],
1800
+ importMarkers: ["dev.openfeature.kotlin.sdk"],
1801
+ calls: typedReads({
1802
+ boolean: ["getBooleanValue", "getBooleanDetails"],
1803
+ string: ["getStringValue", "getStringDetails"],
1804
+ integer: ["getIntegerValue", "getIntegerDetails"],
1805
+ float: ["getDoubleValue", "getDoubleDetails"],
1806
+ json: ["getObjectValue", "getObjectDetails"]
1807
+ }, kotlinLabels)
1808
+ },
1809
+ java: {
1810
+ sdkType: "server",
1811
+ extensions: [".java", ".kt"],
1812
+ importMarkers: ["com.configdirector"],
1813
+ providerPackagesContainingMarkers: ["com.configdirector.openfeature"],
1814
+ projectArea: "jvm-module",
1815
+ calls: [
1816
+ ...typedReads({
1817
+ boolean: ["getBoolean", "watchBoolean"],
1818
+ string: ["getString", "watchString"],
1819
+ integer: ["getInteger", "watchInteger"],
1820
+ float: ["getDouble", "watchDouble"],
1821
+ json: [
1822
+ "getJsonObject",
1823
+ "getJsonArray",
1824
+ "watchJsonObject",
1825
+ "watchJsonArray"
1826
+ ]
1827
+ }),
1828
+ read("getValue"),
1829
+ read("watch"),
1830
+ keyOnly("unwatch"),
1831
+ testMap("createTestClient", "function"),
1832
+ testMethod("setValue"),
1833
+ testMethod("setJsonValue"),
1834
+ testMethod("removeValue"),
1835
+ testMap("replaceValues", "method"),
1836
+ testAnnotation("BooleanConfigValue"),
1837
+ testAnnotation("StringConfigValue"),
1838
+ testAnnotation("IntegerConfigValue"),
1839
+ testAnnotation("FloatConfigValue"),
1840
+ testAnnotation("JsonConfigValue")
1841
+ ]
1842
+ },
1843
+ "openfeature-java": {
1844
+ sdkType: "server",
1845
+ extensions: [".java", ".kt"],
1846
+ importMarkers: ["dev.openfeature.sdk"],
1847
+ calls: typedReads({
1848
+ boolean: ["getBooleanValue", "getBooleanDetails"],
1849
+ string: ["getStringValue", "getStringDetails"],
1850
+ integer: [
1851
+ "getIntegerValue",
1852
+ "getIntegerDetails",
1853
+ "getLongValue",
1854
+ "getLongDetails"
1855
+ ],
1856
+ float: ["getDoubleValue", "getDoubleDetails"],
1857
+ json: ["getObjectValue", "getObjectDetails"]
1858
+ })
1859
+ },
1860
+ python: {
1861
+ sdkType: "server",
1862
+ extensions: [".py"],
1863
+ importMarkers: ["import configdirector", "from configdirector"],
1864
+ providerPackagesContainingMarkers: ["configdirector_openfeature"],
1865
+ calls: [
1866
+ read("get_value", pythonLabels),
1867
+ read("watch", pythonLabels),
1868
+ keyOnly("unwatch", pythonLabels.key),
1869
+ testMap("create_test_client", "function", {
1870
+ position: 0,
1871
+ label: "values"
1872
+ }),
1873
+ testMethod("set_value", {
1874
+ position: 0,
1875
+ label: "key"
1876
+ }),
1877
+ testMethod("remove_value", {
1878
+ position: 0,
1879
+ label: "key"
1880
+ }),
1881
+ testMap("replace_values", "method", {
1882
+ position: 0,
1883
+ label: "values"
1884
+ })
1885
+ ]
1886
+ },
1887
+ "openfeature-python": {
1888
+ sdkType: "server",
1889
+ extensions: [".py"],
1890
+ importMarkers: ["import openfeature", "from openfeature"],
1891
+ calls: typedReads({
1892
+ boolean: [
1893
+ "get_boolean_value",
1894
+ "get_boolean_details",
1895
+ "get_boolean_value_async",
1896
+ "get_boolean_details_async"
1897
+ ],
1898
+ string: [
1899
+ "get_string_value",
1900
+ "get_string_details",
1901
+ "get_string_value_async",
1902
+ "get_string_details_async"
1903
+ ],
1904
+ integer: [
1905
+ "get_integer_value",
1906
+ "get_integer_details",
1907
+ "get_integer_value_async",
1908
+ "get_integer_details_async"
1909
+ ],
1910
+ float: [
1911
+ "get_float_value",
1912
+ "get_float_details",
1913
+ "get_float_value_async",
1914
+ "get_float_details_async"
1915
+ ],
1916
+ json: [
1917
+ "get_object_value",
1918
+ "get_object_details",
1919
+ "get_object_value_async",
1920
+ "get_object_details_async"
1921
+ ]
1922
+ }, openFeaturePythonLabels)
1923
+ },
1924
+ dotnet: {
1925
+ sdkType: "server",
1926
+ extensions: [".cs"],
1927
+ importMarkers: [
1928
+ "using ConfigDirector",
1929
+ "ConfigDirectorClient",
1930
+ "ConfigDirectorTesting"
1931
+ ],
1932
+ providerPackagesContainingMarkers: ["ConfigDirector.OpenFeature"],
1933
+ calls: [
1934
+ read("GetValue", csharpLabels),
1935
+ read("Watch", csharpLabels),
1936
+ ...typedReads({ json: ["GetJsonValue", "WatchJson"] }, csharpLabels),
1937
+ keyOnly("Unwatch", csharpLabels.key),
1938
+ testMap("CreateTestClient", "function", {
1939
+ position: 0,
1940
+ label: "values"
1941
+ }),
1942
+ testMethod("SetValue", {
1943
+ position: 0,
1944
+ label: "key"
1945
+ }),
1946
+ testMethod("RemoveValue", {
1947
+ position: 0,
1948
+ label: "key"
1949
+ }),
1950
+ testMap("ReplaceValues", "method", {
1951
+ position: 0,
1952
+ label: "values"
1953
+ })
1954
+ ]
1955
+ },
1956
+ "openfeature-dotnet": {
1957
+ sdkType: "server",
1958
+ extensions: [".cs"],
1959
+ importMarkers: ["using OpenFeature", "FeatureClient"],
1960
+ calls: typedReads({
1961
+ boolean: ["GetBooleanValueAsync", "GetBooleanDetailsAsync"],
1962
+ string: ["GetStringValueAsync", "GetStringDetailsAsync"],
1963
+ integer: ["GetIntegerValueAsync", "GetIntegerDetailsAsync"],
1964
+ float: ["GetDoubleValueAsync", "GetDoubleDetailsAsync"],
1965
+ json: ["GetObjectValueAsync", "GetObjectDetailsAsync"]
1966
+ }, openFeatureCsharpLabels)
1967
+ },
1968
+ go: {
1969
+ sdkType: "server",
1970
+ extensions: [".go"],
1971
+ importMarkers: ["github.com/ConfigDirector/go-sdks/configdirector"],
1972
+ providerPackagesContainingMarkers: ["github.com/ConfigDirector/go-sdks/configdirector-openfeature"],
1973
+ calls: [
1974
+ ...typedReads({
1975
+ boolean: ["BoolValue", "BoolValueDetails"],
1976
+ string: ["StringValue", "StringValueDetails"],
1977
+ integer: ["IntValue", "IntValueDetails"],
1978
+ float: ["FloatValue", "FloatValueDetails"],
1979
+ json: ["JSONValue", "JSONValueDetails"]
1980
+ }, { argumentCount: {
1981
+ minimum: 3,
1982
+ maximum: 3
1983
+ } }),
1984
+ ...typedReads({
1985
+ boolean: ["WatchBool"],
1986
+ string: ["WatchString"],
1987
+ integer: ["WatchInt"],
1988
+ float: ["WatchFloat"],
1989
+ json: ["WatchJSON"]
1990
+ }, { argumentCount: {
1991
+ minimum: 4,
1992
+ maximum: 4
1993
+ } }),
1994
+ testMap("NewTestClient", "function"),
1995
+ testMethod("SetValue"),
1996
+ testMethod("RemoveValue"),
1997
+ testMap("ReplaceValues", "method")
1998
+ ]
1999
+ },
2000
+ "openfeature-go": {
2001
+ sdkType: "server",
2002
+ extensions: [".go"],
2003
+ importMarkers: ["github.com/open-feature/go-sdk/openfeature"],
2004
+ calls: typedReads({
2005
+ boolean: [
2006
+ "Boolean",
2007
+ "BooleanValue",
2008
+ "BooleanValueDetails"
2009
+ ],
2010
+ string: [
2011
+ "String",
2012
+ "StringValue",
2013
+ "StringValueDetails"
2014
+ ],
2015
+ integer: [
2016
+ "Int",
2017
+ "IntValue",
2018
+ "IntValueDetails"
2019
+ ],
2020
+ float: [
2021
+ "Float",
2022
+ "FloatValue",
2023
+ "FloatValueDetails"
2024
+ ],
2025
+ json: [
2026
+ "Object",
2027
+ "ObjectValue",
2028
+ "ObjectValueDetails"
2029
+ ]
2030
+ }, openFeatureGoArguments)
2031
+ }
2032
+ };
2033
+ const allSdkFamilies = Object.keys(sdkFamilies);
2034
+ //#endregion
2035
+ //#region src/scan/map-literals.ts
2036
+ const KOTLIN_MAP_BUILDERS = new Set([
2037
+ "mapOf",
2038
+ "mutableMapOf",
2039
+ "hashMapOf",
2040
+ "linkedMapOf"
2041
+ ]);
2042
+ const itemsOfGroup = (code, opener) => opener === void 0 ? void 0 : code.commaSeparatedItems(opener);
2043
+ const splitAt = (item, separator) => {
2044
+ if (separator === void 0 || separator === item.first || separator === item.last) return {
2045
+ key: item,
2046
+ keyIsName: false,
2047
+ value: void 0
2048
+ };
2049
+ return {
2050
+ key: {
2051
+ first: item.first,
2052
+ last: separator - 1
2053
+ },
2054
+ keyIsName: false,
2055
+ value: {
2056
+ first: separator + 1,
2057
+ last: item.last
2058
+ }
2059
+ };
2060
+ };
2061
+ const entriesSeparatedByColon = (code, items) => items.map((item) => splitAt(item, code.firstTopLevelIndex(item, (index) => code.isPunctuation(index, ":"))));
2062
+ const entriesInClosingBraces = (code, range) => {
2063
+ const items = itemsOfGroup(code, code.openerOfLastToken(range, "}"));
2064
+ return items === void 0 ? void 0 : entriesSeparatedByColon(code, items);
2065
+ };
2066
+ const javascriptEntry = (code, item) => {
2067
+ const first = code.token(item.first);
2068
+ if (item.first === item.last && first?.kind === "identifier") return {
2069
+ key: item,
2070
+ keyIsName: true,
2071
+ value: item
2072
+ };
2073
+ const entry = splitAt(item, code.firstTopLevelIndex(item, (index) => code.isPunctuation(index, ":")));
2074
+ return {
2075
+ ...entry,
2076
+ keyIsName: entry.key.first === entry.key.last && first?.kind === "identifier"
2077
+ };
2078
+ };
2079
+ const kotlinMapBuilderOpener = (code, range) => {
2080
+ const opener = code.openerOfLastToken(range, ")");
2081
+ const builder = code.token(range.first);
2082
+ return opener !== void 0 && builder?.kind === "identifier" && KOTLIN_MAP_BUILDERS.has(builder.text) ? opener : void 0;
2083
+ };
2084
+ const javaMapOfOpener = (code, range) => code.isIdentifier(range.first, "Map") && code.isPunctuation(range.first + 1, ".") && code.isIdentifier(range.first + 2, "of") && code.openerOfLastToken(range, ")") === range.first + 3 ? range.first + 3 : void 0;
2085
+ const alternatingEntries = (items) => items.flatMap((item, position) => position % 2 === 0 ? [{
2086
+ key: item,
2087
+ keyIsName: false,
2088
+ value: items[position + 1]
2089
+ }] : []);
2090
+ const csharpEntry = (code, item) => {
2091
+ const indexerCloser = code.isPunctuation(item.first, "[") ? code.closerOf(item.first) : void 0;
2092
+ if (indexerCloser !== void 0 && code.isPunctuation(indexerCloser + 1, "=")) return {
2093
+ key: {
2094
+ first: item.first + 1,
2095
+ last: indexerCloser - 1
2096
+ },
2097
+ keyIsName: false,
2098
+ value: {
2099
+ first: indexerCloser + 2,
2100
+ last: item.last
2101
+ }
2102
+ };
2103
+ const [key, value] = (code.isPunctuation(item.first, "{") ? itemsOfGroup(code, item.first) : void 0) ?? [];
2104
+ return key === void 0 ? {
2105
+ key: item,
2106
+ keyIsName: false,
2107
+ value: void 0
2108
+ } : {
2109
+ key,
2110
+ keyIsName: false,
2111
+ value
2112
+ };
2113
+ };
2114
+ const mapLiteralEntries = {
2115
+ javascript: (code, range) => code.openerOfLastToken(range, "}") === range.first ? itemsOfGroup(code, range.first)?.map((item) => javascriptEntry(code, item)) : void 0,
2116
+ python: (code, range) => code.openerOfLastToken(range, "}") === range.first ? entriesSeparatedByColon(code, itemsOfGroup(code, range.first) ?? []) : void 0,
2117
+ dart: entriesInClosingBraces,
2118
+ swift: (code, range) => code.openerOfLastToken(range, "]") === range.first ? entriesSeparatedByColon(code, (itemsOfGroup(code, range.first) ?? []).filter((item) => !code.isPunctuation(item.first, ":"))) : void 0,
2119
+ go: entriesInClosingBraces,
2120
+ kotlin: (code, range) => itemsOfGroup(code, kotlinMapBuilderOpener(code, range))?.map((item) => splitAt(item, code.firstTopLevelIndex(item, (index) => code.isIdentifier(index, "to")))),
2121
+ java: (code, range) => {
2122
+ const items = itemsOfGroup(code, javaMapOfOpener(code, range));
2123
+ return items === void 0 ? void 0 : alternatingEntries(items);
2124
+ },
2125
+ csharp: (code, range) => itemsOfGroup(code, code.openerOfLastToken(range, "}"))?.map((item) => csharpEntry(code, item))
2126
+ };
2127
+ //#endregion
2128
+ //#region src/scan/sdk-calls.ts
2129
+ const LABEL_SEPARATORS = new Set([":", "="]);
2130
+ const CHARACTERS_EXTENDING_A_SEPARATOR = new Set([
2131
+ ":",
2132
+ "=",
2133
+ ">"
2134
+ ]);
2135
+ const labelledArgument = (code, item) => {
2136
+ const labelToken = code.token(item.first);
2137
+ const separator = code.token(item.first + 1);
2138
+ return labelToken?.kind === "identifier" && separator?.kind === "punctuation" && LABEL_SEPARATORS.has(separator.text) && !CHARACTERS_EXTENDING_A_SEPARATOR.has(code.characterAfter(item.first + 1)) && item.first + 2 <= item.last ? {
2139
+ label: labelToken.text,
2140
+ range: {
2141
+ first: item.first + 2,
2142
+ last: item.last
2143
+ }
2144
+ } : {
2145
+ label: void 0,
2146
+ range: item
2147
+ };
2148
+ };
2149
+ const argumentInSlot = (callArguments, slot) => {
2150
+ const labelled = callArguments.find((argument) => slot.label !== void 0 && argument.label === slot.label);
2151
+ if (labelled !== void 0) return labelled;
2152
+ const positional = slot.position === void 0 ? void 0 : callArguments[slot.position];
2153
+ return positional?.label === void 0 ? positional : void 0;
2154
+ };
2155
+ const precedingTokenMatchesForm = (code, nameIndex, shape) => {
2156
+ switch (shape.form) {
2157
+ case "method": return code.isPunctuation(nameIndex - 1, ".");
2158
+ case "annotation": return code.isPunctuation(nameIndex - 1, "@");
2159
+ case "function": return true;
2160
+ }
2161
+ };
2162
+ const dynamicReferenceOf = ({ line, sdk, call, argument }) => ({
2163
+ line,
2164
+ sdk,
2165
+ call,
2166
+ argument
2167
+ });
2168
+ const RAW_VALUE_ACCESSOR = "rawValue";
2169
+ const rangeInsideBrackets = (code, range) => code.isPunctuation(range.first, "[") && code.closerOf(range.first) === range.last ? {
2170
+ first: range.first + 1,
2171
+ last: range.last - 1
2172
+ } : range;
2173
+ const dottedPathSegments = (code, range) => {
2174
+ const path = rangeInsideBrackets(code, range);
2175
+ const tokenCount = path.last - path.first + 1;
2176
+ if (tokenCount < 1 || tokenCount % 2 === 0) return;
2177
+ const segments = [];
2178
+ for (let index = path.first; index <= path.last; index += 2) {
2179
+ const segment = code.token(index);
2180
+ if (segment?.kind !== "identifier" || index < path.last && !code.isPunctuation(index + 1, ".")) return;
2181
+ segments.push(segment.text);
2182
+ }
2183
+ return segments;
2184
+ };
2185
+ const nameReadForKey = (code, range) => {
2186
+ const segments = dottedPathSegments(code, range);
2187
+ if (segments === void 0) return;
2188
+ const accessesRawValue = segments.length > 1 && segments[segments.length - 1] === RAW_VALUE_ACCESSOR;
2189
+ return segments[segments.length - (accessesRawValue ? 2 : 1)];
2190
+ };
2191
+ const TOKENS_SEARCHED_FOR_THE_END_OF_TYPE_ARGUMENTS = 64;
2192
+ const TOKENS_ENDING_A_SEARCH_FOR_TYPE_ARGUMENTS = new Set([
2193
+ "(",
2194
+ ")",
2195
+ "{",
2196
+ "}",
2197
+ ";"
2198
+ ]);
2199
+ const indexAfterTypeArguments = (code, openingAngleBracket) => {
2200
+ let depth = 0;
2201
+ const lastSearched = openingAngleBracket + TOKENS_SEARCHED_FOR_THE_END_OF_TYPE_ARGUMENTS;
2202
+ for (let index = openingAngleBracket; index <= lastSearched; index += 1) {
2203
+ const token = code.token(index);
2204
+ if (token === void 0 || token.kind === "punctuation" && TOKENS_ENDING_A_SEARCH_FOR_TYPE_ARGUMENTS.has(token.text)) return;
2205
+ if (code.isPunctuation(index, "<")) depth += 1;
2206
+ else if (code.isPunctuation(index, ">")) {
2207
+ depth -= 1;
2208
+ if (depth === 0) return index + 1;
2209
+ }
2210
+ }
2211
+ };
2212
+ const openingParenthesisAfterName = (code, nameIndex) => {
2213
+ const afterName = code.opensTypeArguments(nameIndex + 1) ? indexAfterTypeArguments(code, nameIndex + 1) : nameIndex + 1;
2214
+ return afterName !== void 0 && code.isPunctuation(afterName, "(") ? afterName : void 0;
2215
+ };
2216
+ const mapKeyExpressions = (code, range, property) => {
2217
+ const entries = mapLiteralEntries[code.language](code, range);
2218
+ if (entries === void 0) return [{
2219
+ range,
2220
+ isName: false
2221
+ }];
2222
+ if (property === void 0) return entries.map((entry) => ({
2223
+ range: entry.key,
2224
+ isName: entry.keyIsName
2225
+ }));
2226
+ const propertyEntry = entries.find((entry) => literalTextOf(code, {
2227
+ range: entry.key,
2228
+ isName: entry.keyIsName
2229
+ }) === property);
2230
+ return propertyEntry?.value === void 0 ? [] : mapKeyExpressions(code, propertyEntry.value, void 0);
2231
+ };
2232
+ const keyExpressionsOf = (code, callArguments, keyArgument) => {
2233
+ const argument = argumentInSlot(callArguments, keyArgument.slot);
2234
+ if (argument === void 0) return [];
2235
+ switch (keyArgument.kind) {
2236
+ case "key": return [{
2237
+ range: argument.range,
2238
+ isName: false
2239
+ }];
2240
+ case "mapKeys": return mapKeyExpressions(code, argument.range, keyArgument.property);
2241
+ }
2242
+ };
2243
+ const callArgumentsOf = (code, nameIndex) => {
2244
+ const openingParenthesis = openingParenthesisAfterName(code, nameIndex);
2245
+ return (openingParenthesis === void 0 ? void 0 : code.commaSeparatedItems(openingParenthesis))?.map((item) => labelledArgument(code, item));
2246
+ };
2247
+ const hasArgumentCountOf = (shape, callArguments) => shape.argumentCount === void 0 || callArguments.length >= shape.argumentCount.minimum && callArguments.length <= (shape.argumentCount.maximum ?? Number.POSITIVE_INFINITY);
2248
+ const collectCallReferences = (code, nameIndex, sdk, shape, matches) => {
2249
+ const callArguments = precedingTokenMatchesForm(code, nameIndex, shape) ? callArgumentsOf(code, nameIndex) : void 0;
2250
+ if (callArguments === void 0 || !hasArgumentCountOf(shape, callArguments)) return;
2251
+ const inCodeDefaultArgument = shape.inCodeDefault === void 0 ? void 0 : argumentInSlot(callArguments, shape.inCodeDefault);
2252
+ const inCodeDefault = inCodeDefaultArgument === void 0 ? void 0 : inCodeDefaultValueOf(code, inCodeDefaultArgument.range);
2253
+ for (const expression of keyExpressionsOf(code, callArguments, shape.keyArgument)) {
2254
+ const line = code.lineOf(expression.range.first);
2255
+ const keyLiteral = keyLiteralOf(code, expression);
2256
+ const tokenIndex = expression.range.first;
2257
+ const call = {
2258
+ line,
2259
+ sdk,
2260
+ call: shape.name,
2261
+ inCodeDefault,
2262
+ getterType: shape.getterType,
2263
+ test: shape.test
2264
+ };
2265
+ switch (keyLiteral.kind) {
2266
+ case "key":
2267
+ matches.push({
2268
+ tokenIndex,
2269
+ kind: "direct",
2270
+ reference: {
2271
+ ...call,
2272
+ key: keyLiteral.key,
2273
+ argument: void 0
2274
+ }
2275
+ });
2276
+ break;
2277
+ case "notLiteral": {
2278
+ const reference = {
2279
+ ...call,
2280
+ argument: code.textOf(expression.range)
2281
+ };
2282
+ const name = nameReadForKey(code, expression.range);
2283
+ if (name === void 0) matches.push({
2284
+ tokenIndex,
2285
+ kind: "dynamic",
2286
+ reference: dynamicReferenceOf(reference)
2287
+ });
2288
+ else matches.push({
2289
+ tokenIndex,
2290
+ kind: "named",
2291
+ name,
2292
+ reference
2293
+ });
2294
+ break;
2295
+ }
2296
+ case "outsideKeyGrammar": break;
2297
+ }
2298
+ }
2299
+ };
2300
+ const sdkCallMatches = (code, callShapes) => {
2301
+ const shapesByName = /* @__PURE__ */ new Map();
2302
+ for (const callShape of callShapes) {
2303
+ const shapesNamedAlike = shapesByName.get(callShape.shape.name);
2304
+ if (shapesNamedAlike === void 0) shapesByName.set(callShape.shape.name, [callShape]);
2305
+ else shapesNamedAlike.push(callShape);
2306
+ }
2307
+ const matches = [];
2308
+ code.tokens.forEach((token, index) => {
2309
+ if (token.kind !== "identifier") return;
2310
+ for (const { sdk, shape } of shapesByName.get(token.text) ?? []) collectCallReferences(code, index, sdk, shape, matches);
2311
+ });
2312
+ return matches;
2313
+ };
2314
+ //#endregion
2315
+ //#region src/scan/test-files.ts
2316
+ const TEST_DIRECTORY_NAMES = new Set([
2317
+ "test",
2318
+ "tests",
2319
+ "__tests__",
2320
+ "spec",
2321
+ "specs",
2322
+ "androidtest"
2323
+ ]);
2324
+ const TEST_FILE_NAME_SUFFIXES = [
2325
+ ".test",
2326
+ ".spec",
2327
+ "_test",
2328
+ "_spec"
2329
+ ];
2330
+ const TEST_CLASS_NAME_SUFFIXES = [
2331
+ "Test",
2332
+ "Tests",
2333
+ "Spec"
2334
+ ];
2335
+ const TEST_FILE_NAME_PREFIX = "test_";
2336
+ const withoutExtension = (fileName) => {
2337
+ const dot = fileName.lastIndexOf(".");
2338
+ return dot <= 0 ? fileName : fileName.slice(0, dot);
2339
+ };
2340
+ const isTestFileName = (fileName) => {
2341
+ const name = withoutExtension(fileName);
2342
+ const lowercaseName = name.toLowerCase();
2343
+ return lowercaseName.startsWith(TEST_FILE_NAME_PREFIX) || TEST_FILE_NAME_SUFFIXES.some((suffix) => lowercaseName.endsWith(suffix)) || TEST_CLASS_NAME_SUFFIXES.some((suffix) => name.endsWith(suffix));
2344
+ };
2345
+ const isTestFile = (displayPath) => {
2346
+ const segments = displayPath.split("/");
2347
+ const fileName = segments[segments.length - 1] ?? "";
2348
+ return segments.slice(0, -1).some((directory) => TEST_DIRECTORY_NAMES.has(directory.toLowerCase())) || isTestFileName(fileName);
2349
+ };
2350
+ //#endregion
2351
+ //#region src/scan/code-references.ts
2352
+ const RE_EXPORT_STEPS_FOLLOWED = 2;
2353
+ const byTokenIndex = (first, second) => first.tokenIndex - second.tokenIndex;
2354
+ const matchesIn = (file, code, callShapes, projectKeys, constantNameOfToken) => {
2355
+ const callMatches = callShapes.length === 0 ? [] : sdkCallMatches(code, callShapes);
2356
+ const keyTokensOfCalls = new Set(callMatches.flatMap((match) => match.kind === "direct" ? [match.tokenIndex] : []));
2357
+ const isTest = isTestFile(file.displayPath);
2358
+ const textMatches = knownKeyTextsIn(code, projectKeys, keyTokensOfCalls).map((text) => ({
2359
+ tokenIndex: text.tokenIndex,
2360
+ kind: "indirect",
2361
+ reference: {
2362
+ key: text.key,
2363
+ line: code.lineOf(text.tokenIndex),
2364
+ test: isTest
2365
+ },
2366
+ constantName: constantNameOfToken.get(text.tokenIndex)
2367
+ }));
2368
+ return [...callMatches, ...textMatches].sort(byTokenIndex);
2369
+ };
2370
+ const matchOutcomes = (matches, constantIndex) => {
2371
+ const constantsReadByCalls = new Set(matches.flatMap((match) => match.kind === "named" && constantIndex.keyOf(match.name) !== void 0 ? [match.name] : []));
2372
+ return matches.map(({ file, ...match }) => {
2373
+ switch (match.kind) {
2374
+ case "direct": return {
2375
+ file,
2376
+ kind: "direct",
2377
+ reference: match.reference
2378
+ };
2379
+ case "dynamic": return {
2380
+ file,
2381
+ kind: "dynamic",
2382
+ reference: match.reference
2383
+ };
2384
+ case "named": {
2385
+ const key = constantIndex.keyOf(match.name);
2386
+ return key === void 0 ? {
2387
+ file,
2388
+ kind: "dynamic",
2389
+ reference: dynamicReferenceOf(match.reference)
2390
+ } : {
2391
+ file,
2392
+ kind: "direct",
2393
+ reference: {
2394
+ ...match.reference,
2395
+ key
2396
+ }
2397
+ };
2398
+ }
2399
+ case "indirect": return match.constantName !== void 0 && constantsReadByCalls.has(match.constantName) ? {
2400
+ file,
2401
+ kind: "definitionOfConstantRead"
2402
+ } : {
2403
+ file,
2404
+ kind: "indirect",
2405
+ reference: match.reference
2406
+ };
2407
+ }
2408
+ });
2409
+ };
2410
+ const holdsImportMarkerOf = (contents, family) => {
2411
+ const { importMarkers, providerPackagesContainingMarkers = [] } = sdkFamilies[family];
2412
+ const contentsWithoutProviderPackages = providerPackagesContainingMarkers.reduce((text, providerPackage) => text.replaceAll(providerPackage, ""), contents);
2413
+ return importMarkers.some((marker) => contentsWithoutProviderPackages.includes(marker));
2414
+ };
2415
+ const familiesWithMarkerIn = (contents, families) => families.filter((family) => holdsImportMarkerOf(contents, family));
2416
+ const callsAnyOf = (code, families) => {
2417
+ const callNames = new Set(families.flatMap((family) => sdkFamilies[family].calls.map((shape) => shape.name)));
2418
+ return code.tokens.some((token) => token.kind === "identifier" && callNames.has(token.text));
2419
+ };
2420
+ const familiesOfExtension = (extension) => allSdkFamilies.filter((family) => sdkFamilies[family].extensions.includes(extension));
2421
+ const readReachedFile = async (path, localImports) => {
2422
+ const extension = extname(path);
2423
+ const contents = await readFileWithinSizeLimit(path);
2424
+ if (contents === void 0 || !isSourceExtension(extension)) return {
2425
+ kind: "unmarked",
2426
+ reExportedFiles: []
2427
+ };
2428
+ const families = familiesWithMarkerIn(contents, familiesOfExtension(extension));
2429
+ if (families.length > 0) return {
2430
+ kind: "marked",
2431
+ families
2432
+ };
2433
+ return {
2434
+ kind: "unmarked",
2435
+ reExportedFiles: await localImports.filesReExportedBy(new SourceCode(contents, languageOfExtension[extension]), path)
2436
+ };
2437
+ };
2438
+ const sdkFamiliesUsedIn = (highestDirectory) => {
2439
+ const projectAreas = new ProjectAreas(highestDirectory);
2440
+ const localImports = new LocalImports(highestDirectory);
2441
+ const reachedFileOfPath = /* @__PURE__ */ new Map();
2442
+ const familiesMarkedInPackageOrTargetOfFile = /* @__PURE__ */ new Map();
2443
+ const reachedFileAt = (path) => {
2444
+ const reachedFileReadBefore = reachedFileOfPath.get(path);
2445
+ if (reachedFileReadBefore !== void 0) return reachedFileReadBefore;
2446
+ const reachedFileRead = readReachedFile(path, localImports);
2447
+ reachedFileOfPath.set(path, reachedFileRead);
2448
+ return reachedFileRead;
2449
+ };
2450
+ const familiesReachedThrough = async (path, reExportStepsLeft) => {
2451
+ const reachedFile = await reachedFileAt(path);
2452
+ switch (reachedFile.kind) {
2453
+ case "marked": return reachedFile.families;
2454
+ case "unmarked": {
2455
+ const reExportedFiles = reExportStepsLeft === 0 ? [] : reachedFile.reExportedFiles;
2456
+ return (await Promise.all(reExportedFiles.map((reExportedFile) => familiesReachedThrough(reExportedFile, reExportStepsLeft - 1)))).flat();
2457
+ }
2458
+ }
2459
+ };
2460
+ const familiesMarkedThroughImports = async (file, code) => {
2461
+ const importedPaths = await localImports.filesImportedBy(code, file.path);
2462
+ return (await Promise.all(importedPaths.map((importedPath) => familiesReachedThrough(importedPath, RE_EXPORT_STEPS_FOLLOWED)))).flat();
2463
+ };
2464
+ const familiesMarkedInSamePackageOrTarget = async (file, code) => {
2465
+ const familiesFoundBefore = familiesMarkedInPackageOrTargetOfFile.get(file.path);
2466
+ if (familiesFoundBefore !== void 0) return familiesFoundBefore;
2467
+ const filesOfPackageOrTarget = await localImports.filesOfSamePackageOrTarget(code, file.path);
2468
+ const families = Promise.all(filesOfPackageOrTarget.map((fileOfPackageOrTarget) => familiesReachedThrough(fileOfPackageOrTarget, RE_EXPORT_STEPS_FOLLOWED))).then((familiesOfFiles) => familiesOfFiles.flat());
2469
+ for (const fileOfPackageOrTarget of filesOfPackageOrTarget) familiesMarkedInPackageOrTargetOfFile.set(fileOfPackageOrTarget, families);
2470
+ return families;
2471
+ };
2472
+ const familiesMarkedByOtherFiles = async (file, code) => [...await familiesMarkedThroughImports(file, code), ...await familiesMarkedInSamePackageOrTarget(file, code)];
2473
+ return async (file, code) => {
2474
+ const familiesForExtension = familiesOfExtension(file.extension);
2475
+ const markedHere = familiesWithMarkerIn(file.contents, familiesForExtension);
2476
+ const markedByOtherFiles = callsAnyOf(code, familiesForExtension.filter((family) => !markedHere.includes(family))) ? await familiesMarkedByOtherFiles(file, code) : [];
2477
+ const usedFamilies = [];
2478
+ for (const family of familiesForExtension) {
2479
+ const requiredArea = sdkFamilies[family].projectArea;
2480
+ if ((markedHere.includes(family) || markedByOtherFiles.includes(family)) && (requiredArea === void 0 || await projectAreas.contain(requiredArea, file.path))) usedFamilies.push(family);
2481
+ }
2482
+ return usedFamilies;
2483
+ };
2484
+ };
2485
+ const scanCodeReferences = async (scope, projectKeys) => {
2486
+ const scan = {
2487
+ filesScanned: 0,
2488
+ references: [],
2489
+ dynamicReferences: []
2490
+ };
2491
+ const familiesUsedIn = sdkFamiliesUsedIn(scope.workingDirectory);
2492
+ const constantIndex = new ConstantIndex();
2493
+ const matches = [];
2494
+ for await (const file of scannedFiles(scope)) {
2495
+ scan.filesScanned += 1;
2496
+ const code = new SourceCode(file.contents, languageOfExtension[file.extension]);
2497
+ const callShapes = (await familiesUsedIn(file, code)).flatMap((sdk) => sdkFamilies[sdk].calls.map((shape) => ({
2498
+ sdk,
2499
+ shape
2500
+ })));
2501
+ const constantAssignments = constantAssignmentsIn(code);
2502
+ for (const assignment of constantAssignments) constantIndex.add(assignment);
2503
+ const constantNameOfToken = new Map(constantAssignments.map((assignment) => [assignment.tokenIndex, assignment.name]));
2504
+ matches.push(...matchesIn(file, code, callShapes, projectKeys, constantNameOfToken).map((match) => ({
2505
+ ...match,
2506
+ file: file.displayPath
2507
+ })));
2508
+ }
2509
+ for (const outcome of matchOutcomes(matches, constantIndex)) switch (outcome.kind) {
2510
+ case "direct":
2511
+ scan.references.push({
2512
+ ...outcome.reference,
2513
+ kind: "direct",
2514
+ file: outcome.file
2515
+ });
2516
+ break;
2517
+ case "indirect":
2518
+ scan.references.push({
2519
+ ...outcome.reference,
2520
+ kind: "indirect",
2521
+ file: outcome.file
2522
+ });
2523
+ break;
2524
+ case "dynamic":
2525
+ scan.dynamicReferences.push({
2526
+ ...outcome.reference,
2527
+ file: outcome.file
2528
+ });
2529
+ break;
2530
+ case "definitionOfConstantRead": break;
2531
+ }
2532
+ return scan;
2533
+ };
2534
+ //#endregion
2535
+ //#region src/project-scan.ts
2536
+ const scanProject = async (context, path) => {
2537
+ const { workingDirectory } = context.invocation;
2538
+ const settings = await readProjectSettings(workingDirectory);
2539
+ const projectSlug = requireLinkedProjectSlug(context, settings);
2540
+ const root = await scanRootAt(workingDirectory, path);
2541
+ const client = new AdminApiClient({
2542
+ baseUrl: context.baseUrl,
2543
+ token: await configuredApiToken(context)
2544
+ });
2545
+ const project = await findProjectBySlug(client, projectSlug);
2546
+ const configs = await client.listConfigs(project.id);
2547
+ const projectKeys = new Set(configs.flatMap((config) => [config.key, ...config.deprecatedKeys.map((deprecatedKey) => deprecatedKey.key)]));
2548
+ const codeReferenceScan = await scanCodeReferences({
2549
+ root,
2550
+ workingDirectory,
2551
+ include: settings?.include ?? "everyFile",
2552
+ exclude: settings?.exclude ?? []
2553
+ }, projectKeys);
2554
+ return {
2555
+ project,
2556
+ configs,
2557
+ ignoredKeys: new Set(settings?.ignoreKeys ?? []),
2558
+ scannedLocation: root.description,
2559
+ codeReferenceScan
2560
+ };
2561
+ };
2562
+ //#endregion
2563
+ //#region src/commands/scan.ts
2564
+ const referenceJson = (reference) => {
2565
+ switch (reference.kind) {
2566
+ case "direct": return {
2567
+ key: reference.key,
2568
+ file: reference.file,
2569
+ line: reference.line,
2570
+ kind: reference.kind,
2571
+ sdk: reference.sdk,
2572
+ call: reference.call,
2573
+ ...reference.argument === void 0 ? {} : { argument: reference.argument },
2574
+ ...reference.inCodeDefault === void 0 ? {} : { default: reference.inCodeDefault },
2575
+ ...reference.getterType === void 0 ? {} : { getterType: reference.getterType },
2576
+ test: reference.test
2577
+ };
2578
+ case "indirect": return {
2579
+ key: reference.key,
2580
+ file: reference.file,
2581
+ line: reference.line,
2582
+ kind: reference.kind,
2583
+ test: reference.test
2584
+ };
2585
+ }
2586
+ };
2587
+ const scanJson = (projectSlug, scan) => ({
2588
+ project: projectSlug,
2589
+ filesScanned: scan.filesScanned,
2590
+ references: scan.references.map(referenceJson),
2591
+ dynamicReferences: scan.dynamicReferences.map((reference) => ({
2592
+ file: reference.file,
2593
+ line: reference.line,
2594
+ sdk: reference.sdk,
2595
+ call: reference.call,
2596
+ argument: reference.argument
2597
+ }))
2598
+ });
2599
+ const DYNAMIC_REFERENCES_HEADING = "Dynamic references (calls whose key is not a string literal)";
2600
+ const withCollapsedWhitespace = (text) => {
2601
+ let collapsed = "";
2602
+ let previousWasWhitespace = false;
2603
+ for (const character of text) {
2604
+ const isWhitespace = character.trim() === "";
2605
+ if (!isWhitespace) collapsed += character;
2606
+ else if (!previousWasWhitespace) collapsed += " ";
2607
+ previousWasWhitespace = isWhitespace;
2608
+ }
2609
+ return collapsed;
2610
+ };
2611
+ const describeReference = (reference) => {
2612
+ switch (reference.kind) {
2613
+ case "direct": return `${reference.argument === void 0 ? reference.call : `${reference.call}(${reference.argument})`} (${reference.test ? `${reference.sdk}, test` : reference.sdk})`;
2614
+ case "indirect": return reference.test ? "(indirect, test)" : "(indirect)";
2615
+ }
2616
+ };
2617
+ const nothingToScanText = (scannedLocation) => `No source files to scan in ${scannedLocation}.\n`;
2618
+ const scanText = (scan, scannedLocation) => {
2619
+ if (scan.filesScanned === 0) return nothingToScanText(scannedLocation);
2620
+ const referencesByKey = /* @__PURE__ */ new Map();
2621
+ for (const reference of scan.references) {
2622
+ const referencesToKey = referencesByKey.get(reference.key);
2623
+ if (referencesToKey === void 0) referencesByKey.set(reference.key, [reference]);
2624
+ else referencesToKey.push(reference);
2625
+ }
2626
+ const keySections = [...referencesByKey.keys()].sort().map((key) => [key, ...(referencesByKey.get(key) ?? []).map((reference) => ` ${reference.file}:${reference.line} ${describeReference(reference)}`)].join("\n"));
2627
+ const dynamicSection = scan.dynamicReferences.length === 0 ? [] : [[DYNAMIC_REFERENCES_HEADING, ...scan.dynamicReferences.map((reference) => ` ${reference.file}:${reference.line} ${reference.call}(${withCollapsedWhitespace(reference.argument)}) (${reference.sdk})`)].join("\n")];
2628
+ const directCount = scan.references.filter((reference) => reference.kind === "direct").length;
2629
+ const counts = `${directCount} direct, ${scan.references.length - directCount} indirect and ${scan.dynamicReferences.length} dynamic references in ${scan.filesScanned} scanned ${plural(scan.filesScanned, "file", "files")}.`;
2630
+ return `${[
2631
+ ...keySections,
2632
+ ...dynamicSection,
2633
+ counts
2634
+ ].join("\n\n")}\n`;
2635
+ };
2636
+ const scan = async (context, [path]) => {
2637
+ const { project, scannedLocation, codeReferenceScan } = await scanProject(context, path);
2638
+ return {
2639
+ json: scanJson(project.slug, codeReferenceScan),
2640
+ text: scanText(codeReferenceScan, scannedLocation),
2641
+ exitCode: ExitCode.success
2642
+ };
2643
+ };
2644
+ //#endregion
2645
+ //#region src/validate/findings.ts
2646
+ const servedValueTypesOfConfigTypes = new Map([
2647
+ ["boolean", "boolean"],
2648
+ ["string", "string"],
2649
+ ["url", "string"],
2650
+ ["integer", "integer"],
2651
+ ["float", "float"],
2652
+ ["timespan", "float"],
2653
+ ["json", "json"]
2654
+ ]);
2655
+ const servedValueTypesReadableAs = {
2656
+ boolean: ["boolean"],
2657
+ string: [
2658
+ "string",
2659
+ "string-enum",
2660
+ "number-enum"
2661
+ ],
2662
+ integer: [
2663
+ "integer",
2664
+ "float",
2665
+ "number-enum"
2666
+ ],
2667
+ float: ["float", "number-enum"],
2668
+ number: [
2669
+ "integer",
2670
+ "float",
2671
+ "number-enum"
2672
+ ],
2673
+ json: ["json"]
2674
+ };
2675
+ const servedValueTypesOfEnumValueTypes = new Map([["string", "string-enum"], ["number", "number-enum"]]);
2676
+ const isServedTo = {
2677
+ client: (config) => config.client,
2678
+ server: (config) => config.server
2679
+ };
2680
+ const keyMatchesOfConfigs = (configs) => new Map(configs.flatMap((config) => [[config.key, {
2681
+ kind: "currentKey",
2682
+ config
2683
+ }], ...config.deprecatedKeys.map((deprecatedKey) => [deprecatedKey.key, {
2684
+ kind: "deprecatedKey",
2685
+ config
2686
+ }])]));
2687
+ const typesReadBy = (reference) => [...reference.getterType === void 0 ? [] : [reference.getterType], ...reference.inCodeDefault === void 0 ? [] : [reference.inCodeDefault.type]];
2688
+ const servedValueTypeOf = (config) => isEnumConfig(config) ? servedValueTypesOfEnumValueTypes.get(config.typeOptions.valueType) : servedValueTypesOfConfigTypes.get(config.type);
2689
+ const isAllowedValue = (inCodeDefault, allowedValues) => {
2690
+ switch (inCodeDefault.type) {
2691
+ case "string": return allowedValues.includes(inCodeDefault.value);
2692
+ case "integer":
2693
+ case "float": return allowedValues.some((allowedValue) => Number(allowedValue) === inCodeDefault.value);
2694
+ case "boolean":
2695
+ case "json": return false;
2696
+ }
2697
+ };
2698
+ const valueFindings = (reference, config) => {
2699
+ const servedValueType = servedValueTypeOf(config);
2700
+ if (servedValueType === void 0) return [];
2701
+ const typeReadMismatch = typesReadBy(reference).find((typeRead) => !servedValueTypesReadableAs[typeRead].includes(servedValueType));
2702
+ if (typeReadMismatch !== void 0) return [{
2703
+ kind: "type-mismatch",
2704
+ reference,
2705
+ typeRead: typeReadMismatch,
2706
+ configType: config.type,
2707
+ servedAs: servedValueType
2708
+ }];
2709
+ const { inCodeDefault } = reference;
2710
+ if (!isEnumConfig(config) || inCodeDefault === void 0) return [];
2711
+ const allowedValues = config.typeOptions.values;
2712
+ return isAllowedValue(inCodeDefault, allowedValues) ? [] : [{
2713
+ kind: "enum-value-not-allowed",
2714
+ reference,
2715
+ inCodeDefault,
2716
+ allowedValues
2717
+ }];
2718
+ };
2719
+ const availabilityFindings = (reference, config) => {
2720
+ const { sdkType } = sdkFamilies[reference.sdk];
2721
+ if (reference.test || isServedTo[sdkType](config)) return [];
2722
+ return [{
2723
+ kind: "availability-mismatch",
2724
+ reference,
2725
+ sdkType,
2726
+ configAvailableTo: sdkTypes.filter((servedSdkType) => isServedTo[servedSdkType](config))
2727
+ }];
2728
+ };
2729
+ const configFindings = (reference, config) => [...valueFindings(reference, config), ...availabilityFindings(reference, config)];
2730
+ const findingsOfReference = (reference, keyMatch) => {
2731
+ switch (keyMatch.kind) {
2732
+ case "currentKey": return configFindings(reference, keyMatch.config);
2733
+ case "deprecatedKey": return [{
2734
+ kind: "deprecated-key",
2735
+ reference,
2736
+ currentKey: keyMatch.config.key
2737
+ }, ...configFindings(reference, keyMatch.config)];
2738
+ case "unknownKey": return [{
2739
+ kind: "unknown-key",
2740
+ reference
2741
+ }];
2742
+ }
2743
+ };
2744
+ const checkCodeReferences = (references, configs, ignoredKeys) => {
2745
+ const keyMatches = keyMatchesOfConfigs(configs);
2746
+ const checkedReferences = references.filter((reference) => reference.kind === "direct" && !ignoredKeys.has(reference.key));
2747
+ return {
2748
+ referencesChecked: checkedReferences.length,
2749
+ findings: checkedReferences.flatMap((reference) => findingsOfReference(reference, keyMatches.get(reference.key) ?? { kind: "unknownKey" }))
2750
+ };
2751
+ };
2752
+ //#endregion
2753
+ //#region src/commands/validate.ts
2754
+ const SERVED_VALUE_TYPE_NAMES = {
2755
+ boolean: "a boolean",
2756
+ string: "a string",
2757
+ integer: "an integer",
2758
+ float: "a float",
2759
+ json: "JSON",
2760
+ "string-enum": "strings",
2761
+ "number-enum": "numbers"
2762
+ };
2763
+ const CONFIG_TYPES_SERVED_AS_THEMSELVES = new Set([
2764
+ "boolean",
2765
+ "string",
2766
+ "integer",
2767
+ "float",
2768
+ "json"
2769
+ ]);
2770
+ const describeAvailability = (configAvailableTo) => {
2771
+ const [onlySdkType] = configAvailableTo;
2772
+ return onlySdkType === void 0 ? "served to no SDK" : `${onlySdkType}-only`;
2773
+ };
2774
+ const describeInCodeDefault = (inCodeDefault) => inCodeDefault.type === "string" ? JSON.stringify(inCodeDefault.value) : String(inCodeDefault.value);
2775
+ const describeConfigType = (configType, servedAs) => CONFIG_TYPES_SERVED_AS_THEMSELVES.has(configType) ? configType : `${configType}, served as ${SERVED_VALUE_TYPE_NAMES[servedAs]}`;
2776
+ const FINDING_PRESENTATIONS = {
2777
+ "unknown-key": {
2778
+ heading: "Unknown keys",
2779
+ facts: () => ({}),
2780
+ description: (_finding, projectSlug) => `no config in ${projectSlug} has this key.`
2781
+ },
2782
+ "deprecated-key": {
2783
+ heading: "Deprecated keys",
2784
+ facts: (finding) => ({ currentKey: finding.currentKey }),
2785
+ description: (finding) => `the config was renamed to ${finding.currentKey}.`
2786
+ },
2787
+ "type-mismatch": {
2788
+ heading: "Type mismatches",
2789
+ facts: (finding) => ({
2790
+ configType: finding.configType,
2791
+ servedAs: finding.servedAs,
2792
+ ...finding.reference.inCodeDefault === void 0 ? {} : { default: finding.reference.inCodeDefault },
2793
+ ...finding.reference.getterType === void 0 ? {} : { getterType: finding.reference.getterType }
2794
+ }),
2795
+ description: (finding) => `read as ${finding.typeRead}, but the config's type is ${describeConfigType(finding.configType, finding.servedAs)}.`
2796
+ },
2797
+ "availability-mismatch": {
2798
+ heading: "Availability mismatches",
2799
+ facts: (finding) => ({
2800
+ sdk: finding.reference.sdk,
2801
+ sdkType: finding.sdkType,
2802
+ configAvailableTo: finding.configAvailableTo
2803
+ }),
2804
+ description: (finding) => `read from a ${finding.sdkType} SDK (${finding.reference.sdk}), but the config is ${describeAvailability(finding.configAvailableTo)}.`
2805
+ },
2806
+ "enum-value-not-allowed": {
2807
+ heading: "Enum values not allowed",
2808
+ facts: (finding) => ({
2809
+ default: finding.inCodeDefault,
2810
+ allowedValues: finding.allowedValues
2811
+ }),
2812
+ description: (finding) => `${describeInCodeDefault(finding.inCodeDefault)} is not one of the allowed values: ${finding.allowedValues.join(", ")}.`
2813
+ }
2814
+ };
2815
+ const presentationOf = (finding) => FINDING_PRESENTATIONS[finding.kind];
2816
+ const findingJson = (finding) => ({
2817
+ kind: finding.kind,
2818
+ key: finding.reference.key,
2819
+ file: finding.reference.file,
2820
+ line: finding.reference.line,
2821
+ ...presentationOf(finding).facts(finding)
2822
+ });
2823
+ const findingLine = (finding, projectSlug) => ` ${finding.reference.file}:${finding.reference.line} ${finding.reference.key}: ` + presentationOf(finding).description(finding, projectSlug);
2824
+ const countsText = (scan, check) => {
2825
+ const findingCount = check.findings.length;
2826
+ const problems = findingCount === 0 ? "no problems" : `${findingCount} ${plural(findingCount, "problem", "problems")}`;
2827
+ const checked = `Checked ${check.referencesChecked} direct ${plural(check.referencesChecked, "reference", "references")} in ${scan.filesScanned} scanned ${plural(scan.filesScanned, "file", "files")} and found ${problems}.`;
2828
+ const dynamicCount = scan.dynamicReferences.length;
2829
+ return dynamicCount === 0 ? checked : `${checked} ${dynamicCount} dynamic ${plural(dynamicCount, "reference was", "references were")} not checked.`;
2830
+ };
2831
+ const validateText = (projectSlug, scan, scannedLocation, check) => {
2832
+ if (scan.filesScanned === 0) return nothingToScanText(scannedLocation);
2833
+ return `${[...Object.keys(FINDING_PRESENTATIONS).flatMap((kind) => {
2834
+ const findingsOfKind = check.findings.filter((finding) => finding.kind === kind);
2835
+ return findingsOfKind.length === 0 ? [] : [[FINDING_PRESENTATIONS[kind].heading, ...findingsOfKind.map((finding) => findingLine(finding, projectSlug))].join("\n")];
2836
+ }), countsText(scan, check)].join("\n\n")}\n`;
2837
+ };
2838
+ const validate = async (context, [path]) => {
2839
+ const { project, configs, ignoredKeys, scannedLocation, codeReferenceScan } = await scanProject(context, path);
2840
+ const check = checkCodeReferences(codeReferenceScan.references, configs, ignoredKeys);
2841
+ return {
2842
+ json: {
2843
+ ...scanJson(project.slug, codeReferenceScan),
2844
+ findings: check.findings.map(findingJson)
2845
+ },
2846
+ text: validateText(project.slug, codeReferenceScan, scannedLocation, check),
2847
+ exitCode: check.findings.length === 0 ? ExitCode.success : ExitCode.problemsFound
2848
+ };
2849
+ };
2850
+ //#endregion
2851
+ //#region src/run.ts
2852
+ const DEFAULT_BASE_URL = "https://api.configdirector.com";
2853
+ const withoutTrailingSlashes = (url) => {
2854
+ let trimmedUrl = url;
2855
+ while (trimmedUrl.endsWith("/")) trimmedUrl = trimmedUrl.slice(0, -1);
2856
+ return trimmedUrl;
2857
+ };
2858
+ const runCommand = async (invocation, command, actionArguments) => {
2859
+ const parsedCommand = actionArguments.at(-1);
2860
+ const options = parsedCommand.optsWithGlobals();
2861
+ const output = await command({
2862
+ invocation,
2863
+ baseUrl: withoutTrailingSlashes(options.baseUrl ?? nonEmptyVariable(invocation, "CONFIGDIRECTOR_BASE_URL") ?? DEFAULT_BASE_URL),
2864
+ tokenOption: options.token,
2865
+ projectSlug: options.project ?? nonEmptyVariable(invocation, "CONFIGDIRECTOR_PROJECT")
2866
+ }, parsedCommand.processedArgs);
2867
+ invocation.stdout.write(options.json ? `${JSON.stringify(output.json, null, 2)}\n` : output.text);
2868
+ return output.exitCode;
2869
+ };
2870
+ const reportFailure = (invocation, error) => {
2871
+ if (error instanceof CommanderError) return error.exitCode === 0 ? ExitCode.success : ExitCode.couldNotRun;
2872
+ if (error instanceof CliError) {
2873
+ invocation.stderr.write(`Error: ${error.message}\n`);
2874
+ return ExitCode.couldNotRun;
2875
+ }
2876
+ invocation.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
2877
+ return ExitCode.couldNotRun;
2878
+ };
2879
+ const run = async (invocation) => {
2880
+ let exitCode = ExitCode.success;
2881
+ const commandAction = (command) => async (...actionArguments) => {
2882
+ exitCode = await runCommand(invocation, command, actionArguments);
2883
+ };
2884
+ const program = new Command("configdirector").description("Work with ConfigDirector configs from the terminal and CI.").addHelpText("afterAll", "\nBeta: commands and output may change before 1.0.").configureHelp({ showGlobalOptions: true }).version(version).option("--token <token>", "API token, used instead of CONFIGDIRECTOR_TOKEN and the token saved by login").option("--base-url <url>", `Admin API host, used instead of CONFIGDIRECTOR_BASE_URL (default ${DEFAULT_BASE_URL})`).option("--project <slug>", "project slug, used instead of CONFIGDIRECTOR_PROJECT").option("--json", "print JSON on stdout").exitOverride().configureOutput({
2885
+ writeOut: (text) => invocation.stdout.write(text),
2886
+ writeErr: (text) => invocation.stderr.write(text)
2887
+ });
2888
+ program.command("login").description("save an API token for later runs, after checking it can list projects").action(commandAction(login));
2889
+ program.command("init").description("write configdirector.json for a project in the current directory").action(commandAction(init));
2890
+ program.command("scan").argument("[path]", "directory or file to scan (default: the current directory)").description("list where the code reads config keys through a ConfigDirector SDK").action(commandAction(scan));
2891
+ program.command("validate").argument("[path]", "directory or file to check (default: the current directory)").description("check every config key the code reads against the project's configs, exiting 1 when one is wrong").action(commandAction(validate));
2892
+ try {
2893
+ await program.parseAsync(invocation.commandLineArguments, { from: "user" });
2894
+ return exitCode;
2895
+ } catch (error) {
2896
+ return reportFailure(invocation, error);
2897
+ }
2898
+ };
2899
+ //#endregion
2900
+ //#region src/main.ts
2901
+ process.exitCode = await run({
2902
+ commandLineArguments: process.argv.slice(2),
2903
+ environmentVariables: process.env,
2904
+ workingDirectory: process.cwd(),
2905
+ homeDirectory: homedir(),
2906
+ platform: process.platform,
2907
+ stdin: process.stdin,
2908
+ stdout: process.stdout,
2909
+ stderr: process.stderr
2910
+ });
2911
+ //#endregion
2912
+ export {};