intor-cli 0.0.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.
- package/README.md +46 -0
- package/package.json +73 -0
- package/src/build/build-schemas/build-schemas.ts +13 -0
- package/src/build/build-schemas/index.ts +1 -0
- package/src/build/build-types/build-types.ts +46 -0
- package/src/build/build-types/index.ts +1 -0
- package/src/build/build-types/output/append-config-block.ts +34 -0
- package/src/build/build-types/output/append-footer.ts +5 -0
- package/src/build/build-types/output/append-header.ts +11 -0
- package/src/build/build-types/output/index.ts +3 -0
- package/src/build/build-types/utils/indent.ts +3 -0
- package/src/build/build-types/utils/render-infer-node.ts +29 -0
- package/src/build/index.ts +3 -0
- package/src/build/types.ts +19 -0
- package/src/cli/commands/check.ts +50 -0
- package/src/cli/commands/generate.ts +61 -0
- package/src/cli/index.ts +24 -0
- package/src/core/collect-messages/collect-runtime-messages.ts +76 -0
- package/src/core/collect-messages/index.ts +1 -0
- package/src/core/collect-messages/readers.ts +23 -0
- package/src/core/collect-messages/resolve-messages-reader.ts +57 -0
- package/src/core/constants/extra-exts.ts +2 -0
- package/src/core/constants/generated-files.ts +3 -0
- package/src/core/constants/index.ts +7 -0
- package/src/core/diagnostics/collect.ts +59 -0
- package/src/core/diagnostics/group.ts +41 -0
- package/src/core/diagnostics/index.ts +2 -0
- package/src/core/diagnostics/messages.ts +51 -0
- package/src/core/diagnostics/rules/enforce-missing-replacements.ts +55 -0
- package/src/core/diagnostics/rules/enforce-missing-rich.ts +55 -0
- package/src/core/diagnostics/rules/key/empty.ts +34 -0
- package/src/core/diagnostics/rules/key/index.ts +2 -0
- package/src/core/diagnostics/rules/key/not-found.ts +43 -0
- package/src/core/diagnostics/rules/replacement/index.ts +3 -0
- package/src/core/diagnostics/rules/replacement/missing.ts +48 -0
- package/src/core/diagnostics/rules/replacement/not-allowed.ts +43 -0
- package/src/core/diagnostics/rules/replacement/unused.ts +48 -0
- package/src/core/diagnostics/rules/rich/index.ts +3 -0
- package/src/core/diagnostics/rules/rich/missing.ts +48 -0
- package/src/core/diagnostics/rules/rich/not-allowed.ts +43 -0
- package/src/core/diagnostics/rules/rich/unused.ts +48 -0
- package/src/core/diagnostics/types.ts +21 -0
- package/src/core/diagnostics/utils/get-schema-node-at-path.ts +29 -0
- package/src/core/diagnostics/utils/index-usages-by-key.ts +28 -0
- package/src/core/diagnostics/utils/resolve-key-path.ts +5 -0
- package/src/core/discover-configs/discover-configs.ts +87 -0
- package/src/core/discover-configs/index.ts +1 -0
- package/src/core/discover-configs/is-intor-resolved-config.ts +15 -0
- package/src/core/extract-usages/README.md +84 -0
- package/src/core/extract-usages/collectors/collect-key-usages.ts +40 -0
- package/src/core/extract-usages/collectors/collect-pre-keys.ts +58 -0
- package/src/core/extract-usages/collectors/collect-replacement-usages.ts +68 -0
- package/src/core/extract-usages/collectors/collect-rich-usages.ts +57 -0
- package/src/core/extract-usages/collectors/collect-translator-bindings.ts +56 -0
- package/src/core/extract-usages/collectors/index.ts +5 -0
- package/src/core/extract-usages/collectors/utils/extract-static-object-keys.ts +31 -0
- package/src/core/extract-usages/collectors/utils/get-config-key.ts +15 -0
- package/src/core/extract-usages/collectors/utils/get-object-arg.ts +42 -0
- package/src/core/extract-usages/collectors/utils/is-static-string-literal.ts +26 -0
- package/src/core/extract-usages/collectors/utils/walk-translator-bindings.ts +47 -0
- package/src/core/extract-usages/collectors/utils/walk-translator-method-calls.ts +42 -0
- package/src/core/extract-usages/extract-usages.ts +88 -0
- package/src/core/extract-usages/index.ts +13 -0
- package/src/core/extract-usages/load-source-files-from-tscofnig.ts +76 -0
- package/src/core/extract-usages/translator-registry.ts +20 -0
- package/src/core/extract-usages/types.ts +53 -0
- package/src/core/index.ts +13 -0
- package/src/core/infer-schema/index.ts +3 -0
- package/src/core/infer-schema/infer-schemas.ts +16 -0
- package/src/core/infer-schema/messages/index.ts +1 -0
- package/src/core/infer-schema/messages/infer-messages-schema.ts +64 -0
- package/src/core/infer-schema/replacements/extract-interpolation-names.ts +70 -0
- package/src/core/infer-schema/replacements/index.ts +1 -0
- package/src/core/infer-schema/replacements/infer-replacements-schema.ts +63 -0
- package/src/core/infer-schema/rich/index.ts +1 -0
- package/src/core/infer-schema/rich/infer-rich-schema.ts +66 -0
- package/src/core/infer-schema/types.ts +42 -0
- package/src/core/infer-schema/utils/infer-object.ts +32 -0
- package/src/core/infer-schema/utils/is-message-object.ts +5 -0
- package/src/core/read-generated-schema.ts +42 -0
- package/src/core/scan-logger.ts +10 -0
- package/src/core/write-generated-files.ts +51 -0
- package/src/features/check/check.ts +75 -0
- package/src/features/check/index.ts +1 -0
- package/src/features/check/print-summary.ts +28 -0
- package/src/features/generate/generate.ts +76 -0
- package/src/features/generate/index.ts +1 -0
- package/src/features/generate/print-configs.ts +8 -0
- package/src/features/generate/print-summary.ts +19 -0
- package/src/features/index.ts +2 -0
- package/src/features/print-title.ts +7 -0
- package/src/features/spinner.ts +3 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { InferNode } from "../../infer-schema";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve a dot-separated key path to a schema node.
|
|
5
|
+
*
|
|
6
|
+
* Returns:
|
|
7
|
+
* - InferNode if the path exists
|
|
8
|
+
* - null if any segment is missing or non-object
|
|
9
|
+
*/
|
|
10
|
+
export function getSchemaNodeAtPath(
|
|
11
|
+
schema: InferNode,
|
|
12
|
+
path: string,
|
|
13
|
+
): InferNode | null {
|
|
14
|
+
if (schema.kind !== "object") return null;
|
|
15
|
+
|
|
16
|
+
const segments = path.split(".");
|
|
17
|
+
let node: InferNode = schema;
|
|
18
|
+
|
|
19
|
+
for (const segment of segments) {
|
|
20
|
+
if (node.kind !== "object") return null;
|
|
21
|
+
|
|
22
|
+
const next: InferNode | undefined = node.properties[segment];
|
|
23
|
+
if (!next) return null;
|
|
24
|
+
|
|
25
|
+
node = next;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return node;
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TranslatorMethod } from "../../extract-usages";
|
|
2
|
+
import { resolveKeyPath } from "./resolve-key-path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Index extracted usages by their resolved message key.
|
|
6
|
+
*/
|
|
7
|
+
export function indexUsagesByKey<
|
|
8
|
+
T extends { key: string; preKey?: string; method: TranslatorMethod },
|
|
9
|
+
>(usages: T[]): Map<string, T[]> {
|
|
10
|
+
const map = new Map<string, T[]>();
|
|
11
|
+
|
|
12
|
+
for (const usage of usages) {
|
|
13
|
+
const keyPath = resolveKeyPath(usage.key, usage.preKey);
|
|
14
|
+
const indexKey = `${usage.method}::${keyPath}`;
|
|
15
|
+
|
|
16
|
+
// Append this usage to the existing group for the same key
|
|
17
|
+
const list = map.get(indexKey);
|
|
18
|
+
|
|
19
|
+
if (list) {
|
|
20
|
+
list.push(usage);
|
|
21
|
+
} else {
|
|
22
|
+
// First usage encountered for this message key
|
|
23
|
+
map.set(indexKey, [usage]);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return map;
|
|
28
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fg from "fast-glob";
|
|
4
|
+
import { type IntorResolvedConfig } from "intor";
|
|
5
|
+
import { createLogger } from "../scan-logger";
|
|
6
|
+
import { isIntorResolvedConfig } from "./is-intor-resolved-config";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_PATTERNS = ["**/*.{ts,js}"];
|
|
9
|
+
const DEFAULT_IGNORE = [
|
|
10
|
+
"**/node_modules/**",
|
|
11
|
+
"**/dist/**",
|
|
12
|
+
"**/*.d.ts",
|
|
13
|
+
"**/*.test.*",
|
|
14
|
+
"**/*.test-d.ts",
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export interface ConfigEntry {
|
|
18
|
+
filePath: string;
|
|
19
|
+
config: IntorResolvedConfig;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Discover and resolve Intor configs from the current workspace.
|
|
24
|
+
*/
|
|
25
|
+
export async function discoverConfigs(debug?: boolean): Promise<ConfigEntry[]> {
|
|
26
|
+
const files = await fg(DEFAULT_PATTERNS, { ignore: DEFAULT_IGNORE });
|
|
27
|
+
const log = createLogger(debug);
|
|
28
|
+
log("scan", `found ${files.length} candidate files`);
|
|
29
|
+
|
|
30
|
+
const configEntries: ConfigEntry[] = [];
|
|
31
|
+
|
|
32
|
+
// Iterate through candidate files
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const absPath = path.resolve(process.cwd(), file);
|
|
35
|
+
const relPath = path.relative(process.cwd(), absPath);
|
|
36
|
+
|
|
37
|
+
// ----------------------------------------------------------------------
|
|
38
|
+
// Read file content
|
|
39
|
+
// ----------------------------------------------------------------------
|
|
40
|
+
let content: string;
|
|
41
|
+
try {
|
|
42
|
+
content = await fs.promises.readFile(absPath, "utf8");
|
|
43
|
+
} catch {
|
|
44
|
+
log("warn", `failed to read ${relPath}`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ----------------------------------------------------------------------
|
|
49
|
+
// Skip files that clearly do not define an Intor config
|
|
50
|
+
// ----------------------------------------------------------------------
|
|
51
|
+
if (!content.includes("defineIntorConfig(")) {
|
|
52
|
+
log("skip", `${relPath} (no defineIntorConfig)`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
log("load", relPath);
|
|
56
|
+
|
|
57
|
+
// ----------------------------------------------------------------------
|
|
58
|
+
// Dynamic import & export inspection
|
|
59
|
+
// ----------------------------------------------------------------------
|
|
60
|
+
try {
|
|
61
|
+
const moduleExports = await import(absPath);
|
|
62
|
+
let matched = false;
|
|
63
|
+
|
|
64
|
+
for (const module of Object.values(moduleExports)) {
|
|
65
|
+
const config = module as IntorResolvedConfig;
|
|
66
|
+
if (!isIntorResolvedConfig(config)) continue;
|
|
67
|
+
|
|
68
|
+
matched = true;
|
|
69
|
+
|
|
70
|
+
configEntries.push({ filePath: absPath, config });
|
|
71
|
+
log(" ok ", `resolved config "${config.id}"`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!matched) {
|
|
75
|
+
log("warn", `no valid Intor config export found in ${relPath}`);
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
log("warn", `failed to import ${relPath}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (configEntries.length === 0) {
|
|
83
|
+
log("info", "no Intor config discovered");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return configEntries;
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { discoverConfigs, type ConfigEntry } from "./discover-configs";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { IntorResolvedConfig } from "intor";
|
|
2
|
+
|
|
3
|
+
export function isIntorResolvedConfig(
|
|
4
|
+
value: unknown,
|
|
5
|
+
): value is IntorResolvedConfig {
|
|
6
|
+
if (!value || typeof value !== "object") return false;
|
|
7
|
+
|
|
8
|
+
const config = value as IntorResolvedConfig;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
typeof config.id === "string" &&
|
|
12
|
+
typeof config.defaultLocale === "string" &&
|
|
13
|
+
Array.isArray(config.supportedLocales)
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
## Extract Targets
|
|
2
|
+
|
|
3
|
+
#### • Translator Usages (collectTranslatorUsages)
|
|
4
|
+
|
|
5
|
+
Extracts translator method bindings only from destructured calls of registered translator factories.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// ✅ Supported
|
|
9
|
+
const { t, hasKey } = useTranslator();
|
|
10
|
+
t("home.title");
|
|
11
|
+
hasKey("home.title");
|
|
12
|
+
|
|
13
|
+
// ❌ Not supported
|
|
14
|
+
const translator = useTranslator();
|
|
15
|
+
translator.t("home.title");
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
#### • Key Usages (collectKeyUsages)
|
|
19
|
+
|
|
20
|
+
Extracts static string literal keys from the first argument of registered translator method calls.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
// ✅ Supported
|
|
24
|
+
t("title");
|
|
25
|
+
t(`title`);
|
|
26
|
+
|
|
27
|
+
// ❌ Not supported
|
|
28
|
+
t(key);
|
|
29
|
+
t(getTitle());
|
|
30
|
+
t(`title.${key}`);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
#### • Replacement Usages (collectReplacementUsages)
|
|
34
|
+
|
|
35
|
+
Extracts static replacement keys based on method semantics.
|
|
36
|
+
|
|
37
|
+
- For `t`: from the last object-literal argument.
|
|
38
|
+
- For `tRich`: from the object-literal argument following rich tag definitions.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// ✅ Supported
|
|
42
|
+
t("title", { name: "John", count: 3 });
|
|
43
|
+
t("title", options, { name: "John" });
|
|
44
|
+
|
|
45
|
+
// ❌ Not supported
|
|
46
|
+
t("title");
|
|
47
|
+
t("title", replacements);
|
|
48
|
+
t("title", { name: getName() });
|
|
49
|
+
tRich("title", { link: () => null });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### • Rich Usages (collectRichUsages)
|
|
53
|
+
|
|
54
|
+
Extracts static rich tag names from `tRich` calls only.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
// ✅ Supported
|
|
58
|
+
tRich("title", {
|
|
59
|
+
link: () => <a>Link</a>,
|
|
60
|
+
strong: () => <strong>Text</strong>,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ❌ Not supported
|
|
64
|
+
tRich("title", richTags);
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### • PreKeys (collectPreKeys)
|
|
68
|
+
|
|
69
|
+
1. If the first argument is a static string literal, use it as preKey.
|
|
70
|
+
2. Otherwise, if the last argument is an object literal containing a static preKey, use it.
|
|
71
|
+
3. Ignore all other cases.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// ✅ Supported (positional)
|
|
75
|
+
const { t, hasKey } = useTranslator("home");
|
|
76
|
+
|
|
77
|
+
// ✅ Supported (options object)
|
|
78
|
+
const { tRich } = getTranslator(_, _, { preKey: "dashboard" });
|
|
79
|
+
|
|
80
|
+
// ❌ Not supported
|
|
81
|
+
const { t } = useTranslator(prefix);
|
|
82
|
+
const { t } = getTranslator({ preKey: dynamic });
|
|
83
|
+
const translator = useTranslator("home"); // non-destructured binding
|
|
84
|
+
```
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { KeyUsage, TranslatorBindingMap } from "../types";
|
|
2
|
+
import type { SourceFile } from "ts-morph";
|
|
3
|
+
import { isStaticStringLiteral } from "./utils/is-static-string-literal";
|
|
4
|
+
import { walkTranslatorMethodCalls } from "./utils/walk-translator-method-calls";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Collect static translation key usages from translator method calls
|
|
8
|
+
* within a single source file.
|
|
9
|
+
*/
|
|
10
|
+
export function collectKeyUsages(
|
|
11
|
+
sourceFile: SourceFile,
|
|
12
|
+
translatorBindingMap: TranslatorBindingMap,
|
|
13
|
+
): KeyUsage[] {
|
|
14
|
+
const keyUsages: KeyUsage[] = [];
|
|
15
|
+
|
|
16
|
+
walkTranslatorMethodCalls(
|
|
17
|
+
sourceFile,
|
|
18
|
+
translatorBindingMap,
|
|
19
|
+
({ sourceFile, translatorUsage, call, localName }) => {
|
|
20
|
+
const firstArg = call.getArguments()[0];
|
|
21
|
+
if (!isStaticStringLiteral(firstArg)) return;
|
|
22
|
+
|
|
23
|
+
// Resolve source location for diagnostics
|
|
24
|
+
const pos = sourceFile.getLineAndColumnAtPos(firstArg.getStart());
|
|
25
|
+
|
|
26
|
+
keyUsages.push({
|
|
27
|
+
configKey: translatorUsage.configKey,
|
|
28
|
+
factory: translatorUsage.factory,
|
|
29
|
+
method: translatorUsage.method,
|
|
30
|
+
localName,
|
|
31
|
+
key: firstArg.getLiteralText(),
|
|
32
|
+
file: sourceFile.getFilePath(),
|
|
33
|
+
line: pos.line,
|
|
34
|
+
column: pos.column,
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return keyUsages;
|
|
40
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { PreKeyMap, TranslatorBindingMap } from "../types";
|
|
2
|
+
import { SyntaxKind, type SourceFile } from "ts-morph";
|
|
3
|
+
import { getObjectArg } from "./utils/get-object-arg";
|
|
4
|
+
import { isStaticStringLiteral } from "./utils/is-static-string-literal";
|
|
5
|
+
import { walkTranslatorBindings } from "./utils/walk-translator-bindings";
|
|
6
|
+
|
|
7
|
+
const PREKEY_PROPERTY_NAME = "preKey";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Collect static preKey values associated with translator bindings
|
|
11
|
+
* within a single source file.
|
|
12
|
+
*/
|
|
13
|
+
export function collectPreKeys(
|
|
14
|
+
sourceFile: SourceFile,
|
|
15
|
+
translatorBindingMap: TranslatorBindingMap,
|
|
16
|
+
): PreKeyMap {
|
|
17
|
+
const preKeyMap = new Map<string, string>();
|
|
18
|
+
|
|
19
|
+
walkTranslatorBindings(sourceFile, ({ call, binding }) => {
|
|
20
|
+
// Iterate over destructured translator binding elements (e.g. { t, hasKey, ... })
|
|
21
|
+
for (const el of binding.getElements()) {
|
|
22
|
+
const localName = el.getNameNode().getText(); // `t` from `const { t } = useTranslator`
|
|
23
|
+
if (!translatorBindingMap.has(localName)) continue;
|
|
24
|
+
|
|
25
|
+
// -----------------------------------------------------------------------
|
|
26
|
+
// Resolve static preKey from translator factory arguments
|
|
27
|
+
// -----------------------------------------------------------------------
|
|
28
|
+
// 1. From the first positional string argument (e.g. `useTranslator("preKey")`)
|
|
29
|
+
const firstArg = call.getArguments()[0];
|
|
30
|
+
if (isStaticStringLiteral(firstArg)) {
|
|
31
|
+
preKeyMap.set(localName, firstArg.getLiteralText());
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2. From the last options object (e.g. `getTranslator(_, { preKey })`)
|
|
36
|
+
const lastArg = getObjectArg(call, "last");
|
|
37
|
+
if (!lastArg) continue;
|
|
38
|
+
|
|
39
|
+
// Extract the `preKey` property from the options object
|
|
40
|
+
const prop = lastArg.getProperty(PREKEY_PROPERTY_NAME);
|
|
41
|
+
if (!prop || !prop.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
42
|
+
|
|
43
|
+
// Only accept static string initializers as preKey values
|
|
44
|
+
const value = prop.getInitializer();
|
|
45
|
+
if (!value) continue;
|
|
46
|
+
if (
|
|
47
|
+
!value.isKind(SyntaxKind.StringLiteral) &&
|
|
48
|
+
!value.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)
|
|
49
|
+
) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
preKeyMap.set(localName, value.getLiteralText());
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return preKeyMap;
|
|
58
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReplacementUsage, TranslatorBindingMap } from "../types";
|
|
2
|
+
import type { ObjectLiteralExpression, SourceFile } from "ts-morph";
|
|
3
|
+
import { TRANSLATOR_METHOD } from "../translator-registry";
|
|
4
|
+
import { extractStaticObjectKeys } from "./utils/extract-static-object-keys";
|
|
5
|
+
import { getObjectArg } from "./utils/get-object-arg";
|
|
6
|
+
import { isStaticStringLiteral } from "./utils/is-static-string-literal";
|
|
7
|
+
import { walkTranslatorMethodCalls } from "./utils/walk-translator-method-calls";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Collect static replacement usages from translator method calls
|
|
11
|
+
* within a single source file.
|
|
12
|
+
*/
|
|
13
|
+
export function collectReplacementUsages(
|
|
14
|
+
sourceFile: SourceFile,
|
|
15
|
+
translatorBindingMap: TranslatorBindingMap,
|
|
16
|
+
): ReplacementUsage[] {
|
|
17
|
+
const replacementUsages: ReplacementUsage[] = [];
|
|
18
|
+
|
|
19
|
+
walkTranslatorMethodCalls(
|
|
20
|
+
sourceFile,
|
|
21
|
+
translatorBindingMap,
|
|
22
|
+
({ sourceFile, translatorUsage, call, localName }) => {
|
|
23
|
+
if (translatorUsage.method !== "t" && translatorUsage.method !== "tRich")
|
|
24
|
+
return;
|
|
25
|
+
|
|
26
|
+
const firstArg = call.getArguments()[0];
|
|
27
|
+
if (!isStaticStringLiteral(firstArg)) return;
|
|
28
|
+
|
|
29
|
+
// ----------------------------------------------------------------------
|
|
30
|
+
// Resolve replacements based on method semantics
|
|
31
|
+
// ----------------------------------------------------------------------
|
|
32
|
+
let replacementArg: ObjectLiteralExpression | null = null;
|
|
33
|
+
|
|
34
|
+
if (translatorUsage.method === TRANSLATOR_METHOD.t) {
|
|
35
|
+
replacementArg = getObjectArg(call, 2);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (translatorUsage.method === TRANSLATOR_METHOD.tRich) {
|
|
39
|
+
replacementArg = getObjectArg(call, 3);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!replacementArg) return;
|
|
43
|
+
|
|
44
|
+
// ----------------------------------------------------------------------
|
|
45
|
+
// Extract static replacement keys from the replacements object
|
|
46
|
+
// ----------------------------------------------------------------------
|
|
47
|
+
const keys: string[] = extractStaticObjectKeys(replacementArg);
|
|
48
|
+
if (keys.length === 0) return;
|
|
49
|
+
|
|
50
|
+
// Resolve source location for diagnostics
|
|
51
|
+
const pos = sourceFile.getLineAndColumnAtPos(replacementArg.getStart());
|
|
52
|
+
|
|
53
|
+
replacementUsages.push({
|
|
54
|
+
configKey: translatorUsage.configKey,
|
|
55
|
+
factory: translatorUsage.factory,
|
|
56
|
+
method: translatorUsage.method,
|
|
57
|
+
localName,
|
|
58
|
+
key: firstArg.getLiteralText(),
|
|
59
|
+
replacements: keys,
|
|
60
|
+
file: sourceFile.getFilePath(),
|
|
61
|
+
line: pos.line,
|
|
62
|
+
column: pos.column,
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
return replacementUsages;
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { RichUsage, TranslatorBindingMap } from "../types";
|
|
2
|
+
import type { SourceFile } from "ts-morph";
|
|
3
|
+
import { extractStaticObjectKeys } from "./utils/extract-static-object-keys";
|
|
4
|
+
import { getObjectArg } from "./utils/get-object-arg";
|
|
5
|
+
import { isStaticStringLiteral } from "./utils/is-static-string-literal";
|
|
6
|
+
import { walkTranslatorMethodCalls } from "./utils/walk-translator-method-calls";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collect static rich tag usages from translator method calls
|
|
10
|
+
* within a single source file.
|
|
11
|
+
*/
|
|
12
|
+
export function collectRichUsages(
|
|
13
|
+
sourceFile: SourceFile,
|
|
14
|
+
translatorBindingMap: TranslatorBindingMap,
|
|
15
|
+
): RichUsage[] {
|
|
16
|
+
const richUsages: RichUsage[] = [];
|
|
17
|
+
|
|
18
|
+
walkTranslatorMethodCalls(
|
|
19
|
+
sourceFile,
|
|
20
|
+
translatorBindingMap,
|
|
21
|
+
({ sourceFile, translatorUsage, call, localName }) => {
|
|
22
|
+
if (translatorUsage.method !== "tRich") return;
|
|
23
|
+
|
|
24
|
+
const firstArg = call.getArguments()[0];
|
|
25
|
+
if (!isStaticStringLiteral(firstArg)) return;
|
|
26
|
+
|
|
27
|
+
// ----------------------------------------------------------------------
|
|
28
|
+
// Resolve rich tags from the first object-literal argument after the key
|
|
29
|
+
// ----------------------------------------------------------------------
|
|
30
|
+
const richArg = getObjectArg(call, 2);
|
|
31
|
+
if (!richArg) return;
|
|
32
|
+
|
|
33
|
+
// ----------------------------------------------------------------------
|
|
34
|
+
// Extract static rich tag definitions from the rich object argument
|
|
35
|
+
// ----------------------------------------------------------------------
|
|
36
|
+
const keys: string[] = extractStaticObjectKeys(richArg);
|
|
37
|
+
if (keys.length === 0) return;
|
|
38
|
+
|
|
39
|
+
// Resolve source location for diagnostics
|
|
40
|
+
const pos = sourceFile.getLineAndColumnAtPos(richArg.getStart());
|
|
41
|
+
|
|
42
|
+
richUsages.push({
|
|
43
|
+
configKey: translatorUsage.configKey,
|
|
44
|
+
factory: translatorUsage.factory,
|
|
45
|
+
method: translatorUsage.method,
|
|
46
|
+
localName,
|
|
47
|
+
key: firstArg.getLiteralText(),
|
|
48
|
+
rich: keys,
|
|
49
|
+
file: sourceFile.getFilePath(),
|
|
50
|
+
line: pos.line,
|
|
51
|
+
column: pos.column,
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return richUsages;
|
|
57
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { TranslatorBinding, TranslatorBindingMap } from "../types";
|
|
2
|
+
import { SyntaxKind, type SourceFile } from "ts-morph";
|
|
3
|
+
import { getConfigKey } from "../../../core/extract-usages/collectors/utils/get-config-key";
|
|
4
|
+
import {
|
|
5
|
+
TRANSLATOR_FACTORIES,
|
|
6
|
+
TRANSLATOR_METHODS,
|
|
7
|
+
type TranslatorFactory,
|
|
8
|
+
type TranslatorMethod,
|
|
9
|
+
} from "../translator-registry";
|
|
10
|
+
import { walkTranslatorBindings } from "./utils/walk-translator-bindings";
|
|
11
|
+
|
|
12
|
+
export const TRANSLATOR_FACTORIES_SET = new Set<TranslatorFactory>(
|
|
13
|
+
TRANSLATOR_FACTORIES,
|
|
14
|
+
);
|
|
15
|
+
export const TRANSLATOR_METHODS_SET = new Set<TranslatorMethod>(
|
|
16
|
+
TRANSLATOR_METHODS,
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Collect static translator bindings from supported translator factories
|
|
21
|
+
* within a single source file.
|
|
22
|
+
*/
|
|
23
|
+
export function collectTranslatorBindings(
|
|
24
|
+
sourceFile: SourceFile,
|
|
25
|
+
): TranslatorBindingMap {
|
|
26
|
+
const translatorBindingMap = new Map<string, TranslatorBinding>();
|
|
27
|
+
|
|
28
|
+
walkTranslatorBindings(sourceFile, ({ call, binding }) => {
|
|
29
|
+
// ----------------------------------------------------------------------
|
|
30
|
+
// Resolve the translator factory name and ensure it is supported
|
|
31
|
+
// ----------------------------------------------------------------------
|
|
32
|
+
const expr = call.getExpression();
|
|
33
|
+
if (!expr.isKind(SyntaxKind.Identifier)) return;
|
|
34
|
+
const factoryName = expr.getText() as TranslatorFactory;
|
|
35
|
+
if (!TRANSLATOR_FACTORIES_SET.has(factoryName)) return;
|
|
36
|
+
|
|
37
|
+
// Iterate over destructured translator binding elements (e.g. { t, hasKey, ... })
|
|
38
|
+
for (const el of binding.getElements()) {
|
|
39
|
+
// ----------------------------------------------------------------------
|
|
40
|
+
// Resolve the translator method name from the destructured binding
|
|
41
|
+
// ----------------------------------------------------------------------
|
|
42
|
+
const localName = el.getNameNode().getText(); // `t` from `const { t } = useTranslator`
|
|
43
|
+
const aliasName = el.getPropertyNameNode()?.getText(); // `translate` from `const { t: translate } = useTranslator`
|
|
44
|
+
const methodName = (aliasName ?? localName) as TranslatorMethod; // aliasName > originalName
|
|
45
|
+
if (!TRANSLATOR_METHODS_SET.has(methodName)) continue;
|
|
46
|
+
|
|
47
|
+
translatorBindingMap.set(localName, {
|
|
48
|
+
factory: factoryName,
|
|
49
|
+
method: methodName,
|
|
50
|
+
configKey: getConfigKey(call),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return translatorBindingMap;
|
|
56
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { collectTranslatorBindings } from "./collect-translator-bindings";
|
|
2
|
+
export { collectKeyUsages } from "./collect-key-usages";
|
|
3
|
+
export { collectReplacementUsages } from "./collect-replacement-usages";
|
|
4
|
+
export { collectRichUsages } from "./collect-rich-usages";
|
|
5
|
+
export { collectPreKeys } from "./collect-pre-keys";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { SyntaxKind, type ObjectLiteralExpression } from "ts-morph";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extract static property keys from an object literal expression.
|
|
5
|
+
*/
|
|
6
|
+
export function extractStaticObjectKeys(
|
|
7
|
+
obj: ObjectLiteralExpression,
|
|
8
|
+
): string[] {
|
|
9
|
+
const keys: string[] = [];
|
|
10
|
+
|
|
11
|
+
// Walk through object literal properties
|
|
12
|
+
for (const prop of obj.getProperties()) {
|
|
13
|
+
// Only support simple property assignments (skip spreads, methods, etc.)
|
|
14
|
+
if (!prop.isKind(SyntaxKind.PropertyAssignment)) continue;
|
|
15
|
+
|
|
16
|
+
const nameNode = prop.getNameNode();
|
|
17
|
+
|
|
18
|
+
// { foo: ... }
|
|
19
|
+
if (nameNode.isKind(SyntaxKind.Identifier)) {
|
|
20
|
+
keys.push(nameNode.getText());
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// { "foo": ... }
|
|
25
|
+
if (nameNode.isKind(SyntaxKind.StringLiteral)) {
|
|
26
|
+
keys.push(nameNode.getLiteralText());
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return keys;
|
|
31
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SyntaxKind, type CallExpression } from "ts-morph";
|
|
2
|
+
|
|
3
|
+
export function getConfigKey(call: CallExpression): string | undefined {
|
|
4
|
+
const typeArgs = call.getTypeArguments();
|
|
5
|
+
if (typeArgs.length === 0) return undefined;
|
|
6
|
+
|
|
7
|
+
const firstArg = typeArgs[0];
|
|
8
|
+
|
|
9
|
+
if (!firstArg.isKind(SyntaxKind.LiteralType)) return undefined;
|
|
10
|
+
|
|
11
|
+
const literal = firstArg.getLiteral();
|
|
12
|
+
if (!literal || !literal.isKind(SyntaxKind.StringLiteral)) return undefined;
|
|
13
|
+
|
|
14
|
+
return literal.getLiteralText();
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CallExpression,
|
|
3
|
+
Node,
|
|
4
|
+
ObjectLiteralExpression,
|
|
5
|
+
ts,
|
|
6
|
+
} from "ts-morph";
|
|
7
|
+
import { SyntaxKind } from "ts-morph";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* position:
|
|
11
|
+
* - "first" | "last"
|
|
12
|
+
* - number: 1-based argument position (2 = second argument)
|
|
13
|
+
*/
|
|
14
|
+
type ObjectArgPosition = "first" | "last" | number;
|
|
15
|
+
|
|
16
|
+
export function getObjectArg(
|
|
17
|
+
node: CallExpression,
|
|
18
|
+
position: ObjectArgPosition,
|
|
19
|
+
): ObjectLiteralExpression | null {
|
|
20
|
+
const args = node.getArguments();
|
|
21
|
+
|
|
22
|
+
let target: Node<ts.Node> | undefined | null = null;
|
|
23
|
+
|
|
24
|
+
switch (position) {
|
|
25
|
+
case "first": {
|
|
26
|
+
target = args[0];
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
case "last": {
|
|
31
|
+
target = args.at(-1);
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
default: {
|
|
36
|
+
target = args[position - 1];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!target?.isKind(SyntaxKind.ObjectLiteralExpression)) return null;
|
|
41
|
+
return target;
|
|
42
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Node,
|
|
3
|
+
NoSubstitutionTemplateLiteral,
|
|
4
|
+
StringLiteral,
|
|
5
|
+
ts,
|
|
6
|
+
} from "ts-morph";
|
|
7
|
+
import { SyntaxKind } from "ts-morph";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Check whether a node is a static string literal.
|
|
11
|
+
*
|
|
12
|
+
* Supports:
|
|
13
|
+
* - "text"
|
|
14
|
+
* - 'text'
|
|
15
|
+
* - \`text\` (no substitutions)
|
|
16
|
+
*/
|
|
17
|
+
export function isStaticStringLiteral(
|
|
18
|
+
node: Node<ts.Node> | undefined,
|
|
19
|
+
): node is StringLiteral | NoSubstitutionTemplateLiteral {
|
|
20
|
+
if (!node) return false;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
node.isKind(SyntaxKind.StringLiteral) ||
|
|
24
|
+
node.isKind(SyntaxKind.NoSubstitutionTemplateLiteral)
|
|
25
|
+
);
|
|
26
|
+
}
|