intor-cli 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/package.json +2 -3
  2. package/src/cli/commands/check.ts +23 -17
  3. package/src/cli/commands/discover.ts +32 -0
  4. package/src/cli/commands/generate.ts +35 -40
  5. package/src/cli/commands/index.ts +4 -0
  6. package/src/cli/commands/options/index.ts +1 -0
  7. package/src/cli/commands/options/options.ts +55 -0
  8. package/src/cli/commands/utils/normalize-message-files.ts +49 -0
  9. package/src/cli/commands/utils/normalize-reader-options.ts +15 -28
  10. package/src/cli/commands/validate.ts +26 -30
  11. package/src/cli/index.ts +38 -23
  12. package/src/cli/menu/index.ts +1 -0
  13. package/src/cli/menu/prompts/prompt-check.ts +74 -0
  14. package/src/cli/menu/prompts/prompt-discover.ts +25 -0
  15. package/src/cli/menu/prompts/prompt-generate.ts +106 -0
  16. package/src/cli/menu/prompts/prompt-validate.ts +49 -0
  17. package/src/cli/menu/prompts/shared/prompt-reader-options.ts +63 -0
  18. package/src/cli/menu/prompts/shared/shared.ts +76 -0
  19. package/src/cli/menu/run.ts +72 -0
  20. package/src/cli/version.ts +3 -0
  21. package/src/constants.ts +6 -0
  22. package/src/core/artifacts/index.ts +5 -0
  23. package/src/core/artifacts/schema/build-schema.ts +13 -0
  24. package/src/core/artifacts/schema/index.ts +3 -0
  25. package/src/core/{generated → artifacts/schema}/read-schema.ts +4 -4
  26. package/src/core/artifacts/schema/write-schema.ts +14 -0
  27. package/src/{build/build-types → core/artifacts/types/build}/build-types.ts +9 -10
  28. package/src/{build/build-types → core/artifacts/types/build}/utils/normalize-rich-infer-node.ts +1 -1
  29. package/src/{build/build-types → core/artifacts/types/build}/utils/render-infer-node.ts +1 -1
  30. package/src/core/artifacts/types/index.ts +2 -0
  31. package/src/core/artifacts/types/write-types.ts +8 -0
  32. package/src/core/artifacts/types.ts +20 -0
  33. package/src/core/collect-messages/collect-other-locale-messages.ts +5 -7
  34. package/src/core/collect-messages/collect-runtime-messages.ts +8 -6
  35. package/src/core/collect-messages/index.ts +1 -0
  36. package/src/core/collect-messages/readers.ts +1 -0
  37. package/src/core/collect-messages/types.ts +7 -1
  38. package/src/core/constants/index.ts +2 -0
  39. package/src/core/discover-configs/discover-configs.ts +47 -26
  40. package/src/core/extract-usages/extract-usages.ts +33 -24
  41. package/src/core/index.ts +12 -7
  42. package/src/core/infer-shape/index.ts +4 -0
  43. package/src/core/{infer-schema/messages/infer-messages-schema.ts → infer-shape/infer-messages-shape.ts} +5 -10
  44. package/src/core/{infer-schema/replacements/infer-replacements-schema.ts → infer-shape/infer-replacements-shape.ts} +6 -11
  45. package/src/core/{infer-schema/rich/infer-rich-schema.ts → infer-shape/infer-rich-shape.ts} +5 -10
  46. package/src/core/infer-shape/infer-shapes.ts +21 -0
  47. package/src/core/{infer-schema → infer-shape}/types.ts +4 -4
  48. package/src/core/scan/index.ts +2 -0
  49. package/src/core/{extract-usages/load-source-files-from-tscofnig.ts → scan/load-source-files.ts} +34 -15
  50. package/src/core/scan/scan-files.ts +25 -0
  51. package/src/features/check/build-scoped-usages.ts +35 -0
  52. package/src/features/check/check.ts +51 -53
  53. package/src/features/check/diagnostics/collect.ts +6 -2
  54. package/src/features/check/diagnostics/group.ts +0 -1
  55. package/src/features/check/index.ts +1 -0
  56. package/src/features/check/render-config-summary.ts +47 -0
  57. package/src/features/check/types.ts +12 -0
  58. package/src/features/discover/discover.ts +22 -0
  59. package/src/features/discover/index.ts +1 -0
  60. package/src/features/generate/generate.ts +56 -49
  61. package/src/features/generate/index.ts +1 -0
  62. package/src/features/generate/render-overrides.ts +73 -0
  63. package/src/features/generate/render-summary.ts +28 -0
  64. package/src/features/generate/types.ts +12 -0
  65. package/src/features/generate/utils/resolve-message-source.ts +20 -0
  66. package/src/features/generate/utils/validate-message-source.ts +53 -0
  67. package/src/features/index.ts +4 -3
  68. package/src/features/shared/to-relative-path.ts +10 -0
  69. package/src/features/shared/write-json-report.ts +19 -0
  70. package/src/features/validate/index.ts +1 -0
  71. package/src/features/validate/{messages/validate-messages-schema.ts → missing/collect-missing-messages.ts} +5 -5
  72. package/src/features/validate/{replacements/validate-replacements-schema.ts → missing/collect-missing-replacements.ts} +4 -4
  73. package/src/features/validate/missing/collect-missing-requirements.ts +44 -0
  74. package/src/features/validate/{rich/validate-rich-schema.ts → missing/collect-missing-rich.ts} +5 -5
  75. package/src/features/validate/render-config-summary.ts +47 -0
  76. package/src/features/validate/render-locale-blocks.ts +56 -0
  77. package/src/features/validate/types.ts +14 -0
  78. package/src/features/validate/validate.ts +38 -43
  79. package/src/logger.ts +95 -0
  80. package/src/render.ts +57 -0
  81. package/src/build/build-schemas/build-schemas.ts +0 -13
  82. package/src/build/build-schemas/index.ts +0 -1
  83. package/src/build/index.ts +0 -3
  84. package/src/build/types.ts +0 -20
  85. package/src/core/generated/index.ts +0 -6
  86. package/src/core/generated/write-messages-snapshot.ts +0 -27
  87. package/src/core/generated/write-schema.ts +0 -9
  88. package/src/core/generated/write-types.ts +0 -8
  89. package/src/core/infer-schema/index.ts +0 -4
  90. package/src/core/infer-schema/infer-schemas.ts +0 -20
  91. package/src/core/infer-schema/messages/index.ts +0 -1
  92. package/src/core/infer-schema/replacements/index.ts +0 -1
  93. package/src/core/infer-schema/rich/index.ts +0 -1
  94. package/src/core/scan-logger.ts +0 -10
  95. package/src/features/check/print-summary.ts +0 -28
  96. package/src/features/generate/print-configs.ts +0 -8
  97. package/src/features/generate/print-overrides.ts +0 -62
  98. package/src/features/generate/print-summary.ts +0 -26
  99. package/src/features/print.ts +0 -43
  100. package/src/features/validate/messages/index.ts +0 -1
  101. package/src/features/validate/print-summary.ts +0 -65
  102. package/src/features/validate/replacements/index.ts +0 -1
  103. package/src/features/validate/rich/index.ts +0 -1
  104. package/src/features/validate/validate-locale-messages.ts +0 -38
  105. /package/src/core/{generated → artifacts}/ensure-and-write.ts +0 -0
  106. /package/src/{build/build-types → core/artifacts/types/build}/index.ts +0 -0
  107. /package/src/{build/build-types → core/artifacts/types/build}/output/append-config-block.ts +0 -0
  108. /package/src/{build/build-types → core/artifacts/types/build}/output/append-footer.ts +0 -0
  109. /package/src/{build/build-types → core/artifacts/types/build}/output/append-header.ts +0 -0
  110. /package/src/{build/build-types → core/artifacts/types/build}/output/index.ts +0 -0
  111. /package/src/{build/build-types → core/artifacts/types/build}/utils/indent.ts +0 -0
  112. /package/src/core/{infer-schema/replacements → infer-shape/utils}/extract-interpolation-names.ts +0 -0
  113. /package/src/core/{infer-schema → infer-shape}/utils/infer-object.ts +0 -0
  114. /package/src/core/{infer-schema → infer-shape}/utils/is-message-object.ts +0 -0
  115. /package/src/core/{infer-schema → infer-shape}/utils/should-skip-key.ts +0 -0
  116. /package/src/core/{infer-schema → infer-shape}/utils/strip-internal-keys.ts +0 -0
  117. /package/src/features/{spinner.ts → shared/spinner.ts} +0 -0
@@ -1,20 +1,12 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import fg from "fast-glob";
4
3
  import { type IntorResolvedConfig } from "intor";
5
- import { createLogger } from "../scan-logger";
4
+ import { createLogger } from "../../logger";
5
+ import { br, cyan, yellow } from "../../render";
6
+ import { scanFiles } from "../scan";
6
7
  import { isIntorResolvedConfig } from "./is-intor-resolved-config";
7
8
  import { loadModule } from "./load-module";
8
9
 
9
- const DEFAULT_PATTERNS = ["**/*.{ts,js}"];
10
- const DEFAULT_IGNORE = [
11
- "**/node_modules/**",
12
- "**/dist/**",
13
- "**/*.d.ts",
14
- "**/*.test.*",
15
- "**/*.test-d.ts",
16
- ];
17
-
18
10
  export interface ConfigEntry {
19
11
  filePath: string;
20
12
  config: IntorResolvedConfig;
@@ -22,13 +14,23 @@ export interface ConfigEntry {
22
14
 
23
15
  /**
24
16
  * Discover and resolve Intor configs from the current workspace.
17
+ *
18
+ * Notes:
19
+ * - Configs must be declared via `defineIntorConfig(...)`
20
+ * - Dynamic or computed configs are intentionally not supported
25
21
  */
26
- export async function discoverConfigs(debug?: boolean): Promise<ConfigEntry[]> {
27
- const files = await fg(DEFAULT_PATTERNS, { ignore: DEFAULT_IGNORE });
28
- const log = createLogger(debug);
29
- log("scan", `found ${files.length} candidate files`);
22
+ export async function discoverConfigs(debug = false): Promise<ConfigEntry[]> {
23
+ const files = await scanFiles();
24
+
25
+ if (debug) br();
26
+ const logger = createLogger(debug);
27
+ logger.header(
28
+ `Discover configs - scanning ${yellow(files.length)} candidate files`,
29
+ { kind: "process", lineBreakAfter: 1 },
30
+ );
30
31
 
31
32
  const configEntries: ConfigEntry[] = [];
33
+ const seenIds = new Set<string>();
32
34
 
33
35
  // Iterate through candidate files
34
36
  for (const file of files) {
@@ -42,7 +44,7 @@ export async function discoverConfigs(debug?: boolean): Promise<ConfigEntry[]> {
42
44
  try {
43
45
  content = await fs.promises.readFile(absPath, "utf8");
44
46
  } catch {
45
- log("warn", `failed to read ${relPath}`);
47
+ logger.process("warn", `failed to read ${relPath}`);
46
48
  continue;
47
49
  }
48
50
 
@@ -50,10 +52,10 @@ export async function discoverConfigs(debug?: boolean): Promise<ConfigEntry[]> {
50
52
  // Skip files that clearly do not define an Intor config
51
53
  // ----------------------------------------------------------------------
52
54
  if (!content.includes("defineIntorConfig(")) {
53
- log("skip", `${relPath} (no defineIntorConfig)`);
55
+ logger.process("skip", `${relPath} (missing defineIntorConfig)`);
54
56
  continue;
55
57
  }
56
- log("load", relPath);
58
+ logger.process("load", relPath);
57
59
 
58
60
  // ----------------------------------------------------------------------
59
61
  // Dynamic import & export inspection
@@ -61,28 +63,47 @@ export async function discoverConfigs(debug?: boolean): Promise<ConfigEntry[]> {
61
63
  try {
62
64
  const moduleExports = await loadModule(absPath);
63
65
  let matched = false;
66
+ let resolvedCount = 0;
64
67
 
68
+ // Loop through all exports
65
69
  for (const module of Object.values(moduleExports)) {
66
70
  const config = module as IntorResolvedConfig;
67
- if (!isIntorResolvedConfig(config)) continue;
68
-
71
+ if (!isIntorResolvedConfig(module)) continue;
69
72
  matched = true;
70
73
 
74
+ // Ensure config ids are unique across the workspace
75
+ if (seenIds.has(module.id)) {
76
+ logger.process(
77
+ "warn",
78
+ `duplicate config id "${module.id}" (ignored, ${relPath})`,
79
+ );
80
+ continue;
81
+ }
82
+
83
+ seenIds.add(module.id);
84
+ resolvedCount++;
85
+
71
86
  configEntries.push({ filePath: absPath, config });
72
- log(" ok ", `resolved config "${config.id}"`);
87
+ logger.process("ok", `resolved config ${cyan(config.id)}`);
73
88
  }
74
89
 
75
- if (!matched) {
76
- log("warn", `no valid Intor config export found in ${relPath}`);
90
+ if (matched && resolvedCount === 0) {
91
+ logger.process("warn", `no usable Intor config export (${relPath})`);
77
92
  }
78
93
  } catch {
79
- log("warn", `failed to import ${relPath}`);
94
+ logger.process("warn", `failed to import module (${relPath})`);
80
95
  }
81
96
  }
82
97
 
83
98
  if (configEntries.length === 0) {
84
- log("info", "no Intor config discovered");
99
+ logger.process("warn", "no Intor config discovered");
85
100
  }
86
101
 
87
- return configEntries;
102
+ logger.footer(
103
+ `scanned ${yellow(files.length)} files, resolved ${yellow(
104
+ configEntries.length,
105
+ )} Intor config(s)`,
106
+ { kind: "process", lineBreakBefore: 1 },
107
+ );
108
+ return configEntries.sort((a, b) => a.filePath.localeCompare(b.filePath));
88
109
  }
@@ -1,8 +1,8 @@
1
1
  import type { ExtractedUsages } from "./types";
2
- import pc from "picocolors";
3
- import { createLogger } from "../scan-logger";
2
+ import type { SourceFile } from "ts-morph";
3
+ import { createLogger } from "../../logger";
4
+ import { br, yellow } from "../../render";
4
5
  import { extractUsagesFromSourceFile } from "./extract-usages-from-source-file";
5
- import { loadSourceFilesFromTsconfig } from "./load-source-files-from-tscofnig";
6
6
 
7
7
  /** Check whether a file-level extraction produced any meaningful usage */
8
8
  const isEmpty = (u: ExtractedUsages) =>
@@ -12,16 +12,23 @@ const isEmpty = (u: ExtractedUsages) =>
12
12
  u.trans.length === 0;
13
13
 
14
14
  export interface ExtractUsagesOptions {
15
- tsconfigPath?: string;
15
+ sourceFiles?: SourceFile[];
16
16
  debug?: boolean;
17
17
  }
18
18
 
19
19
  /**
20
20
  * Extract all static translator usages from a TypeScript project.
21
21
  */
22
- export function extractUsages(options?: ExtractUsagesOptions): ExtractedUsages {
23
- const { tsconfigPath = "tsconfig.json", debug = false } = options || {};
24
- const log = createLogger(debug);
22
+ export function extractUsages({
23
+ sourceFiles = [],
24
+ debug = false,
25
+ }: ExtractUsagesOptions = {}): ExtractedUsages {
26
+ if (debug) br();
27
+ const logger = createLogger(debug);
28
+ logger.header(
29
+ `Extract usages - processing ${yellow(sourceFiles.length)} source files`,
30
+ { kind: "process", lineBreakAfter: 1 },
31
+ );
25
32
 
26
33
  const result: ExtractedUsages = {
27
34
  preKey: [],
@@ -35,36 +42,38 @@ export function extractUsages(options?: ExtractUsagesOptions): ExtractedUsages {
35
42
  let scannedFiles = 0;
36
43
  let matchedFiles = 0;
37
44
 
38
- const sourceFiles = loadSourceFilesFromTsconfig(tsconfigPath, debug);
39
-
40
45
  // Process each source file independently
41
46
  for (const sourceFile of sourceFiles) {
42
47
  scannedFiles++;
43
- log("scan", sourceFile.getFilePath());
44
48
 
45
49
  // ---------------------------------------------------------------------------
46
50
  // File-level extraction (pure analysis, no side effects)
47
51
  // ---------------------------------------------------------------------------
48
- const partial = extractUsagesFromSourceFile(sourceFile);
49
- if (isEmpty(partial)) continue;
52
+ const partialUsages = extractUsagesFromSourceFile(sourceFile);
53
+
54
+ if (isEmpty(partialUsages)) {
55
+ logger.process("skip", sourceFile.getFilePath());
56
+ continue;
57
+ }
58
+
59
+ logger.process("ok", sourceFile.getFilePath());
50
60
  matchedFiles++;
51
61
 
52
62
  // ---------------------------------------------------------------------------
53
63
  // Merge file-level results into the project-level result
54
64
  // ---------------------------------------------------------------------------
55
- result.preKey.push(...partial.preKey);
56
- result.key.push(...partial.key);
57
- result.replacement.push(...partial.replacement);
58
- result.rich.push(...partial.rich);
59
- result.trans.push(...partial.trans);
60
- }
61
-
62
- // Debug summary
63
- if (debug) {
64
- console.log(
65
- pc.dim(` › Scanned ${scannedFiles} files, matched ${matchedFiles} \n`),
66
- );
65
+ result.preKey.push(...partialUsages.preKey);
66
+ result.key.push(...partialUsages.key);
67
+ result.replacement.push(...partialUsages.replacement);
68
+ result.rich.push(...partialUsages.rich);
69
+ result.trans.push(...partialUsages.trans);
67
70
  }
68
71
 
72
+ logger.footer(
73
+ `scanned ${yellow(scannedFiles)} files, extracted from ${yellow(
74
+ matchedFiles,
75
+ )} file(s)`,
76
+ { kind: "process", lineBreakBefore: 1 },
77
+ );
69
78
  return result;
70
79
  }
package/src/core/index.ts CHANGED
@@ -1,19 +1,21 @@
1
1
  // discover-configs
2
- export { discoverConfigs } from "./discover-configs";
2
+ export { discoverConfigs, type ConfigEntry } from "./discover-configs";
3
3
 
4
4
  // collect-messages
5
5
  export {
6
6
  collectRuntimeMessages,
7
7
  collectOtherLocaleMessages,
8
+ type ReaderOptions,
9
+ type MergeOverrides,
8
10
  } from "./collect-messages";
9
11
 
10
12
  // infer-schema
11
13
  export {
12
- inferSchemas,
14
+ inferShapes,
13
15
  type InferNode,
14
- type InferredSchemas,
16
+ type InferredShapes,
15
17
  extractInterpolationNames,
16
- } from "./infer-schema";
18
+ } from "./infer-shape";
17
19
 
18
20
  // extract-usages
19
21
  export {
@@ -28,13 +30,16 @@ export {
28
30
  type RichUsage,
29
31
  } from "./extract-usages";
30
32
 
31
- // generated
33
+ // artifacts
32
34
  export {
35
+ buildTypes,
33
36
  writeTypes,
37
+ buildSchema,
34
38
  writeSchema,
35
39
  readSchema,
36
- writeMessagesSnapshot,
37
- } from "./generated";
40
+ type GeneratedSchema,
41
+ type SchemaEntry,
42
+ } from "./artifacts";
38
43
 
39
44
  // constants
40
45
  export { DEFAULT_OUT_DIR, EXTRA_EXTS, type ExtraExt } from "./constants";
@@ -0,0 +1,4 @@
1
+ export { inferShapes } from "./infer-shapes";
2
+ export type { InferNode, InferredShapes } from "./types";
3
+
4
+ export { extractInterpolationNames } from "./utils/extract-interpolation-names";
@@ -1,15 +1,12 @@
1
- import type { InferNode } from "../types";
1
+ import type { InferNode } from "./types";
2
2
  import type { MessageObject, MessageValue } from "intor";
3
- import { inferObject } from "../utils/infer-object";
4
- import { isMessageObject } from "../utils/is-message-object";
3
+ import { inferObject } from "./utils/infer-object";
4
+ import { isMessageObject } from "./utils/is-message-object";
5
5
 
6
6
  /**
7
- * Infer message value types from a message object.
8
- *
9
- * Traverses message values and derives a semantic schema tree.
10
- * Rendering to TypeScript type string should be done in a later stage.
7
+ * Infer the semantic shape of message values from a message object.
11
8
  */
12
- export function inferMessagesSchema(messages: MessageObject): InferNode {
9
+ export function inferMessagesShape(messages: MessageObject): InferNode {
13
10
  if (!isMessageObject(messages) || Object.keys(messages).length === 0) {
14
11
  return { kind: "none" };
15
12
  }
@@ -17,8 +14,6 @@ export function inferMessagesSchema(messages: MessageObject): InferNode {
17
14
  }
18
15
 
19
16
  /**
20
- * Infer a semantic node from a single message value.
21
- *
22
17
  * - Primitive values → primitive node
23
18
  * - Arrays → array node (first-element policy)
24
19
  * - Objects → object node (recursive, empty pruned)
@@ -1,16 +1,13 @@
1
- import type { InferNode } from "../types";
1
+ import type { InferNode } from "./types";
2
2
  import type { MessageObject, MessageValue } from "intor";
3
- import { inferObject } from "../utils/infer-object";
4
- import { isMessageObject } from "../utils/is-message-object";
5
- import { extractInterpolationNames } from "./extract-interpolation-names";
3
+ import { extractInterpolationNames } from "./utils/extract-interpolation-names";
4
+ import { inferObject } from "./utils/infer-object";
5
+ import { isMessageObject } from "./utils/is-message-object";
6
6
 
7
7
  /**
8
- * Infer interpolation replacement schema from a message object.
9
- *
10
- * Traverses message values and extracts `{...}` interpolation names
11
- * into a semantic schema tree.
8
+ * Infer the semantic shape of interpolation replacements from a message object.
12
9
  */
13
- export function inferReplacementsSchema(messages: MessageObject): InferNode {
10
+ export function inferReplacementsShape(messages: MessageObject): InferNode {
14
11
  if (!isMessageObject(messages) || Object.keys(messages).length === 0) {
15
12
  return { kind: "none" };
16
13
  }
@@ -18,8 +15,6 @@ export function inferReplacementsSchema(messages: MessageObject): InferNode {
18
15
  }
19
16
 
20
17
  /**
21
- * Infer replacement information from a single message value.
22
- *
23
18
  * - Strings are scanned for `{...}` interpolations
24
19
  * - Objects are traversed recursively
25
20
  * - Arrays and unsupported values are ignored
@@ -1,16 +1,13 @@
1
- import type { InferNode } from "../types";
1
+ import type { InferNode } from "./types";
2
2
  import type { MessageObject, MessageValue } from "intor";
3
3
  import { tokenize, type Token } from "intor";
4
- import { inferObject } from "../utils/infer-object";
5
- import { isMessageObject } from "../utils/is-message-object";
4
+ import { inferObject } from "./utils/infer-object";
5
+ import { isMessageObject } from "./utils/is-message-object";
6
6
 
7
7
  /**
8
- * Infer rich tag schema from a message object.
9
- *
10
- * Traverses message values and extracts rich tag names
11
- * into a semantic schema tree.
8
+ * Infer the semantic shape of rich tags from a message object.
12
9
  */
13
- export function inferRichSchema(messages: MessageObject): InferNode {
10
+ export function inferRichShape(messages: MessageObject): InferNode {
14
11
  if (!isMessageObject(messages) || Object.keys(messages).length === 0) {
15
12
  return { kind: "none" };
16
13
  }
@@ -18,8 +15,6 @@ export function inferRichSchema(messages: MessageObject): InferNode {
18
15
  }
19
16
 
20
17
  /**
21
- * Infer rich tag information from a single message value.
22
- *
23
18
  * - Strings are tokenized and analyzed for rich tags
24
19
  * - Objects are traversed recursively
25
20
  * - Arrays and unsupported values are ignored
@@ -0,0 +1,21 @@
1
+ import type { InferredShapes } from "./types";
2
+ import type { MessageObject } from "intor";
3
+ import { inferMessagesShape } from "./infer-messages-shape";
4
+ import { inferReplacementsShape } from "./infer-replacements-shape";
5
+ import { inferRichShape } from "./infer-rich-shape";
6
+ import { stripInternalKeys } from "./utils/strip-internal-keys";
7
+
8
+ /**
9
+ * Infer all semantic shapes from messages.
10
+ */
11
+ export function inferShapes(messages: MessageObject): InferredShapes {
12
+ const inferredShapes: InferredShapes = {
13
+ messages: inferMessagesShape(messages),
14
+ replacements: inferReplacementsShape(messages),
15
+ rich: inferRichShape(messages),
16
+ };
17
+
18
+ stripInternalKeys(inferredShapes);
19
+
20
+ return inferredShapes;
21
+ }
@@ -35,8 +35,8 @@ export interface InferRecordNode {
35
35
  kind: "record";
36
36
  }
37
37
 
38
- export interface InferredSchemas {
39
- messagesSchema: InferNode;
40
- replacementsSchema: InferNode;
41
- richSchema: InferNode;
38
+ export interface InferredShapes {
39
+ messages: InferNode;
40
+ replacements: InferNode;
41
+ rich: InferNode;
42
42
  }
@@ -0,0 +1,2 @@
1
+ export { loadSourceFiles } from "./load-source-files";
2
+ export { scanFiles } from "./scan-files";
@@ -1,7 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { Project, type SourceFile } from "ts-morph";
4
- import { createLogger } from "../scan-logger";
4
+ import { createLogger } from "../../logger";
5
+ import { br, yellow } from "../../render";
5
6
 
6
7
  /**
7
8
  * Load source files from a tsconfig.
@@ -16,47 +17,65 @@ import { createLogger } from "../scan-logger";
16
17
  * where the root tsconfig only contains references.
17
18
  * - References are followed non-recursively on purpose.
18
19
  */
19
- export function loadSourceFilesFromTsconfig(
20
+ export function loadSourceFiles(
20
21
  tsconfigPath: string,
21
- debug: boolean,
22
+ debug = false,
22
23
  ): SourceFile[] {
23
- const log = createLogger(debug);
24
+ if (debug) br();
25
+ const logger = createLogger(debug);
26
+ logger.header("Load source files - processing tsconfig", {
27
+ kind: "process",
28
+ });
29
+
24
30
  // ---------------------------------------------------------------------------
25
- // 1. Try loading source files directly from the given tsconfig
31
+ // Try loading source files directly from the given tsconfig
26
32
  // ---------------------------------------------------------------------------
27
33
  const project = new Project({ tsConfigFilePath: tsconfigPath });
28
34
  const files = project.getSourceFiles();
29
- if (files.length > 0) return files;
35
+ if (files.length > 0) {
36
+ logger.footer(`loaded ${yellow(files.length)} files`, { kind: "process" });
37
+ return files;
38
+ }
30
39
 
31
40
  // ---------------------------------------------------------------------------
32
- // 2. No source files found attempt to follow project references
41
+ // No source files found, attempt to follow project references
33
42
  // ---------------------------------------------------------------------------
34
43
  const configDir = path.dirname(tsconfigPath);
35
- const rawConfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf8"));
36
- const references: { path: string }[] = rawConfig.references ?? [];
44
+ const rawTsConfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf8"));
45
+
46
+ // Project references (e.g. { references: [ { path: "./tsconfig.app.json" } ] })
47
+ const references: { path: string }[] = rawTsConfig.references ?? [];
48
+ if (references.length === 0) {
49
+ logger.footer("no source files found", { kind: "process" });
50
+ return [];
51
+ }
37
52
 
38
- if (references.length === 0) return [];
39
- log("info", `no source files found, following project references`);
53
+ logger.log();
54
+ logger.process("load", `references (${references.length})`);
40
55
 
41
56
  // ---------------------------------------------------------------------------
42
- // 3. Load source files from each referenced tsconfig
57
+ // Load source files from each referenced tsconfig
43
58
  // ---------------------------------------------------------------------------
44
59
  const collected: SourceFile[] = [];
45
60
 
46
61
  for (const ref of references) {
47
- const refPath = path.resolve(configDir, ref.path);
62
+ const refPath = path.relative(configDir, ref.path);
48
63
 
49
64
  // Skip missing referenced tsconfig files
50
65
  if (!fs.existsSync(refPath)) {
51
- log("warn", `referenced tsconfig not found: ${refPath}`);
66
+ logger.process("warn", `referenced tsconfig not found: ${refPath}`);
52
67
  continue;
53
68
  }
54
69
 
55
- log("ref ", `${path.relative(process.cwd(), refPath)}`);
70
+ logger.process("load", `${path.relative(process.cwd(), refPath)}`);
56
71
 
57
72
  const refProject = new Project({ tsConfigFilePath: refPath });
58
73
  collected.push(...refProject.getSourceFiles());
59
74
  }
60
75
 
76
+ logger.footer(`loaded ${yellow(collected.length)} files`, {
77
+ kind: "process",
78
+ lineBreakBefore: 1,
79
+ });
61
80
  return collected;
62
81
  }
@@ -0,0 +1,25 @@
1
+ import fg from "fast-glob";
2
+
3
+ const DEFAULT_PATTERNS = ["**/*.{ts,js}"];
4
+
5
+ const DEFAULT_IGNORE = [
6
+ "**/node_modules/**",
7
+ "**/dist/**",
8
+ "**/*.d.ts",
9
+ "**/*.test.*",
10
+ "**/*.test-d.ts",
11
+ ];
12
+
13
+ interface ScanFilesOptions {
14
+ patterns?: string[];
15
+ ignore?: string[];
16
+ cwd?: string;
17
+ }
18
+
19
+ export async function scanFiles({
20
+ patterns = DEFAULT_PATTERNS,
21
+ ignore = DEFAULT_IGNORE,
22
+ cwd = process.cwd(),
23
+ }: ScanFilesOptions = {}) {
24
+ return fg(patterns, { ignore, cwd });
25
+ }
@@ -0,0 +1,35 @@
1
+ import type { ExtractedUsages } from "../../core";
2
+ import { dedupePreKeyUsages } from "./dedupe-pre-key-usages";
3
+
4
+ function resolveConfigKey(
5
+ usageConfigKey: string | undefined,
6
+ defaultConfigKey: string,
7
+ ) {
8
+ if (usageConfigKey === "__default__") return defaultConfigKey;
9
+ if (usageConfigKey == null) return defaultConfigKey;
10
+ return usageConfigKey;
11
+ }
12
+
13
+ /**
14
+ * Build per-config scoped usages from extracted usages
15
+ */
16
+ export const buildScopeUsages = ({
17
+ usages,
18
+ defaultConfigKey,
19
+ configKey,
20
+ }: {
21
+ usages: ExtractedUsages;
22
+ defaultConfigKey: string;
23
+ configKey: string;
24
+ }): ExtractedUsages => {
25
+ const match = (u: { configKey?: string }) =>
26
+ resolveConfigKey(u.configKey, defaultConfigKey) === configKey;
27
+
28
+ return {
29
+ preKey: dedupePreKeyUsages(usages.preKey.filter((el) => match(el))),
30
+ key: usages.key.filter((el) => match(el)),
31
+ replacement: usages.replacement.filter((el) => match(el)),
32
+ rich: usages.rich.filter((el) => match(el)),
33
+ trans: usages.trans.filter((el) => match(el)),
34
+ };
35
+ };