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
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
### Design Guarantees
|
|
2
|
+
|
|
3
|
+
- Message types are inferred from the **_default locale_** only.
|
|
4
|
+
- All locales are assumed to share the same message shape.
|
|
5
|
+
- Locale is treated as a runtime dimension, not a structural one.
|
|
6
|
+
- Generated types are intentionally conservative and do not validate locale completeness.
|
|
7
|
+
|
|
8
|
+
### NOTE
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
(Core Modules)
|
|
12
|
+
├── collect-messages
|
|
13
|
+
├── extract-usages
|
|
14
|
+
├── find-configs
|
|
15
|
+
├── generate-types
|
|
16
|
+
├── infer-schema
|
|
17
|
+
├── validation
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- pipeline: Collect messages
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
input: none (Detect from environment)
|
|
24
|
+
|-> find-configs -> collect-messages
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- pipeline: Generate intor schema
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
intput: collected messages
|
|
31
|
+
|-> infer-schema (write file)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- pipeline: Generate intor types
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
intput: intor schema
|
|
38
|
+
|-> generate-types (write file)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- pipeline: Validation
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
intput: intor schema
|
|
45
|
+
|-> extract-usages -> validation
|
|
46
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "intor-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Intor CLI",
|
|
5
|
+
"author": "Yiming Liao",
|
|
6
|
+
"homepage": "https://github.com/yiming-liao/intor-cli#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/yiming-liao/intor-cli"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/yiming-liao/intor-cli/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"i18n"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"type": "module",
|
|
19
|
+
"files": [
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=16.0.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"intor": "tsx src/cli/index.ts",
|
|
29
|
+
"type": "tsc --noEmit",
|
|
30
|
+
"lint": "eslint",
|
|
31
|
+
"lint:debug": "eslint --debug",
|
|
32
|
+
"test": "vitest",
|
|
33
|
+
"test:check": "tsx scripts/check-test-structure.ts",
|
|
34
|
+
"knip": "knip --config .config/knip.config.ts"
|
|
35
|
+
},
|
|
36
|
+
"bin": {
|
|
37
|
+
"intor": "src/cli/index.ts"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@intor/reader-md": "0.1.1",
|
|
41
|
+
"@intor/reader-yaml": "0.1.0",
|
|
42
|
+
"cac": "6.7.14",
|
|
43
|
+
"fast-glob": "3.3.3",
|
|
44
|
+
"intor": "2.3.17",
|
|
45
|
+
"logry": "2.1.6",
|
|
46
|
+
"ora": "9.0.0",
|
|
47
|
+
"picocolors": "1.1.1",
|
|
48
|
+
"ts-morph": "27.0.2",
|
|
49
|
+
"tsx": "4.21.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "25.0.3",
|
|
53
|
+
"@vitest/coverage-v8": "4.0.16",
|
|
54
|
+
"eslint": "9.39.2",
|
|
55
|
+
"eslint-config-prettier": "10.1.8",
|
|
56
|
+
"eslint-import-resolver-typescript": "4.4.4",
|
|
57
|
+
"eslint-plugin-import": "2.32.0",
|
|
58
|
+
"eslint-plugin-prettier": "5.5.4",
|
|
59
|
+
"eslint-plugin-unicorn": "62.0.0",
|
|
60
|
+
"eslint-plugin-unused-imports": "4.3.0",
|
|
61
|
+
"knip": "5.79.0",
|
|
62
|
+
"prettier": "3.7.4",
|
|
63
|
+
"remark": "15.0.1",
|
|
64
|
+
"remark-gfm": "4.0.1",
|
|
65
|
+
"typescript": "5.9.3",
|
|
66
|
+
"typescript-eslint": "8.51.0",
|
|
67
|
+
"vitest": "4.0.16"
|
|
68
|
+
},
|
|
69
|
+
"peerDependencies": {},
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BuildInput, GeneratedSchema } from "../types";
|
|
2
|
+
|
|
3
|
+
export function buildSchemas(inputs: BuildInput[]): GeneratedSchema {
|
|
4
|
+
return {
|
|
5
|
+
version: 1,
|
|
6
|
+
generatedAt: new Date().toISOString(),
|
|
7
|
+
configs: inputs.map((input) => ({
|
|
8
|
+
id: input.id,
|
|
9
|
+
locales: input.locales,
|
|
10
|
+
schemas: input.schemas,
|
|
11
|
+
})),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildSchemas } from "./build-schemas";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BuildInput } from "../types";
|
|
2
|
+
import { appendHeader, appendConfigBlock, appendFooter } from "./output";
|
|
3
|
+
import { renderInferNode } from "./utils/render-infer-node";
|
|
4
|
+
|
|
5
|
+
const GENERATED_INTERFACE_NAME = "IntorGeneratedTypes";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds the global TypeScript declaration from inferred typegen inputs.
|
|
9
|
+
*/
|
|
10
|
+
export function buildTypes(inputs: BuildInput[]): string {
|
|
11
|
+
const lines: string[] = [];
|
|
12
|
+
|
|
13
|
+
// Global declaration header
|
|
14
|
+
appendHeader(lines, GENERATED_INTERFACE_NAME);
|
|
15
|
+
|
|
16
|
+
// Per-config processing
|
|
17
|
+
for (const [index, input] of inputs.entries()) {
|
|
18
|
+
const localesType = input.locales.map((l) => `"${l}"`).join(" | ");
|
|
19
|
+
const messagesType = renderInferNode(input.schemas.messagesSchema);
|
|
20
|
+
const replacementsType = renderInferNode(input.schemas.replacementsSchema);
|
|
21
|
+
const richType = renderInferNode(input.schemas.richSchema);
|
|
22
|
+
|
|
23
|
+
if (index === 0) {
|
|
24
|
+
appendConfigBlock(lines, {
|
|
25
|
+
id: "__default__",
|
|
26
|
+
locales: localesType,
|
|
27
|
+
messages: messagesType,
|
|
28
|
+
replacements: replacementsType,
|
|
29
|
+
rich: richType,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
appendConfigBlock(lines, {
|
|
34
|
+
id: `"${input.id}"`,
|
|
35
|
+
locales: localesType,
|
|
36
|
+
messages: messagesType,
|
|
37
|
+
replacements: replacementsType,
|
|
38
|
+
rich: richType,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Global declaration footer
|
|
43
|
+
appendFooter(lines);
|
|
44
|
+
|
|
45
|
+
return lines.join("\n");
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildTypes } from "./build-types";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { PREFIX_PLACEHOLDER } from "intor";
|
|
2
|
+
import { indent } from "../utils/indent";
|
|
3
|
+
|
|
4
|
+
function wrapWithLocale(type: string) {
|
|
5
|
+
return `{
|
|
6
|
+
${indent(4)}"${PREFIX_PLACEHOLDER}": ${type};
|
|
7
|
+
${indent(3)}}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function appendConfigBlock(
|
|
11
|
+
lines: string[],
|
|
12
|
+
{
|
|
13
|
+
id,
|
|
14
|
+
locales,
|
|
15
|
+
messages,
|
|
16
|
+
replacements,
|
|
17
|
+
rich,
|
|
18
|
+
}: {
|
|
19
|
+
id: string;
|
|
20
|
+
locales: string;
|
|
21
|
+
messages: string;
|
|
22
|
+
replacements: string;
|
|
23
|
+
rich: string;
|
|
24
|
+
},
|
|
25
|
+
) {
|
|
26
|
+
lines.push(
|
|
27
|
+
`${indent(2)}${id}: {`,
|
|
28
|
+
`${indent(3)}Locales: ${locales};`,
|
|
29
|
+
`${indent(3)}Messages: ${wrapWithLocale(messages)};`,
|
|
30
|
+
`${indent(3)}Replacements: ${wrapWithLocale(replacements)};`,
|
|
31
|
+
`${indent(3)}Rich: ${wrapWithLocale(rich)};`,
|
|
32
|
+
`${indent(2)}};`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { indent } from "../utils/indent";
|
|
2
|
+
|
|
3
|
+
export const INTOR_GENERATED_KEY = "__intor_generated__";
|
|
4
|
+
|
|
5
|
+
export function appendHeader(lines: string[], interfaceName: string) {
|
|
6
|
+
lines.push(
|
|
7
|
+
`declare global {`,
|
|
8
|
+
`${indent(1)}interface ${interfaceName} {`,
|
|
9
|
+
`${indent(2)}${INTOR_GENERATED_KEY}: true;`,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { InferNode } from "../../../core/infer-schema";
|
|
2
|
+
import { indent } from "./indent";
|
|
3
|
+
|
|
4
|
+
export function renderInferNode(node: InferNode, indentLevel = 4): string {
|
|
5
|
+
switch (node.kind) {
|
|
6
|
+
case "none": {
|
|
7
|
+
return "unknown";
|
|
8
|
+
}
|
|
9
|
+
case "primitive": {
|
|
10
|
+
return node.type;
|
|
11
|
+
}
|
|
12
|
+
case "array": {
|
|
13
|
+
return `${renderInferNode(node.element, indentLevel)}[]`;
|
|
14
|
+
}
|
|
15
|
+
case "record": {
|
|
16
|
+
return "Record<string, unknown>";
|
|
17
|
+
}
|
|
18
|
+
case "object": {
|
|
19
|
+
return `{
|
|
20
|
+
${Object.entries(node.properties)
|
|
21
|
+
.map(
|
|
22
|
+
([k, v]) =>
|
|
23
|
+
`${indent(indentLevel)} "${k}": ${renderInferNode(v, indentLevel + 1)};`,
|
|
24
|
+
)
|
|
25
|
+
.join("\n")}
|
|
26
|
+
${indent(indentLevel)}}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { InferredSchemas } from "../core";
|
|
2
|
+
|
|
3
|
+
export interface BuildInput {
|
|
4
|
+
id: string;
|
|
5
|
+
locales: readonly string[];
|
|
6
|
+
schemas: InferredSchemas;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Schema
|
|
10
|
+
export interface GeneratedSchema {
|
|
11
|
+
version: number;
|
|
12
|
+
generatedAt: string;
|
|
13
|
+
configs: GeneratedSchemaConfig[];
|
|
14
|
+
}
|
|
15
|
+
export interface GeneratedSchemaConfig {
|
|
16
|
+
id: string;
|
|
17
|
+
locales: readonly string[];
|
|
18
|
+
schemas: InferredSchemas;
|
|
19
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReadGeneratedSchemaOptions } from "../../core";
|
|
2
|
+
import type { ExtractUsagesOptions } from "../../core/extract-usages/extract-usages";
|
|
3
|
+
import type { CAC } from "cac";
|
|
4
|
+
import { check } from "../../features";
|
|
5
|
+
|
|
6
|
+
export function registerCheckCommand(cli: CAC) {
|
|
7
|
+
cli
|
|
8
|
+
// -----------------------------------------------------------------------
|
|
9
|
+
// Command
|
|
10
|
+
// -----------------------------------------------------------------------
|
|
11
|
+
.command("check", "Validate intor translation usage")
|
|
12
|
+
|
|
13
|
+
// -----------------------------------------------------------------------
|
|
14
|
+
// Option
|
|
15
|
+
// -----------------------------------------------------------------------
|
|
16
|
+
// Read generated schema options
|
|
17
|
+
.option(
|
|
18
|
+
"--cwd <path>",
|
|
19
|
+
"Working directory for resolving intor config (default: process.cwd())",
|
|
20
|
+
)
|
|
21
|
+
.option(
|
|
22
|
+
"--file-name <name>",
|
|
23
|
+
"Generated schema file name (default: intor-generated.schema.json)",
|
|
24
|
+
)
|
|
25
|
+
.option(
|
|
26
|
+
"--out-dir <path>",
|
|
27
|
+
"Directory of generated schema output (default: .intor)",
|
|
28
|
+
)
|
|
29
|
+
// Extract usages options
|
|
30
|
+
.option(
|
|
31
|
+
"--tsconfig <path>",
|
|
32
|
+
"Path to tsconfig.json (default: tsconfig.json)",
|
|
33
|
+
)
|
|
34
|
+
.option("--debug", "Enable debug logging")
|
|
35
|
+
|
|
36
|
+
// -----------------------------------------------------------------------
|
|
37
|
+
// Action
|
|
38
|
+
// -----------------------------------------------------------------------
|
|
39
|
+
.action(
|
|
40
|
+
async (options: ReadGeneratedSchemaOptions & ExtractUsagesOptions) => {
|
|
41
|
+
const { cwd, fileName, outDir, tsconfigPath, debug } = options;
|
|
42
|
+
try {
|
|
43
|
+
await check({ cwd, fileName, outDir }, { tsconfigPath, debug });
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(error);
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ExtraExt } from "../../core";
|
|
2
|
+
import type { CAC } from "cac";
|
|
3
|
+
import { generate } from "../../features";
|
|
4
|
+
|
|
5
|
+
export function registerGenerateCommand(cli: CAC) {
|
|
6
|
+
cli
|
|
7
|
+
// -----------------------------------------------------------------------
|
|
8
|
+
// Command
|
|
9
|
+
// -----------------------------------------------------------------------
|
|
10
|
+
.command("generate", "Generate intor types and schemas")
|
|
11
|
+
|
|
12
|
+
// -----------------------------------------------------------------------
|
|
13
|
+
// Option
|
|
14
|
+
// -----------------------------------------------------------------------
|
|
15
|
+
.option(
|
|
16
|
+
"--ext <ext>",
|
|
17
|
+
"Enable extra messages file extension (repeatable)",
|
|
18
|
+
{ default: [] },
|
|
19
|
+
)
|
|
20
|
+
.option(
|
|
21
|
+
"--reader <mapping>",
|
|
22
|
+
"Custom reader mapping in the form <ext=path> (repeatable)",
|
|
23
|
+
{ default: [] },
|
|
24
|
+
)
|
|
25
|
+
.option(
|
|
26
|
+
"--debug",
|
|
27
|
+
"Print debug information during config discovery and generation",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
// -----------------------------------------------------------------------
|
|
31
|
+
// Action
|
|
32
|
+
// -----------------------------------------------------------------------
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
const { ext, reader, debug } = options as {
|
|
35
|
+
ext?: Array<ExtraExt>;
|
|
36
|
+
reader?: string[];
|
|
37
|
+
debug?: boolean;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Normalize exts
|
|
41
|
+
const exts = ext ? (Array.isArray(ext) ? ext : [ext]) : [];
|
|
42
|
+
|
|
43
|
+
// Parse customReaders: ["md=./my-md-reader.ts"] → { md: "./my-md-reader.ts" }
|
|
44
|
+
let customReaders: Record<string, string> | undefined;
|
|
45
|
+
if (reader && reader.length > 0) {
|
|
46
|
+
customReaders = {};
|
|
47
|
+
for (const item of reader) {
|
|
48
|
+
const [key, value] = item.split("=", 2);
|
|
49
|
+
if (!key || !value) continue;
|
|
50
|
+
customReaders[key] = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await generate({ exts, customReaders, debug });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(error);
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
import { cac } from "cac";
|
|
4
|
+
import { registerCheckCommand } from "./commands/check";
|
|
5
|
+
import { registerGenerateCommand } from "./commands/generate";
|
|
6
|
+
|
|
7
|
+
const cli = cac("intor");
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------
|
|
10
|
+
// Register commands
|
|
11
|
+
// ---------------------------------------------------------------------
|
|
12
|
+
registerGenerateCommand(cli);
|
|
13
|
+
registerCheckCommand(cli);
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------
|
|
16
|
+
// Global options / help
|
|
17
|
+
// ---------------------------------------------------------------------
|
|
18
|
+
cli.help();
|
|
19
|
+
cli.version("0.1.10");
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------
|
|
22
|
+
// Parse argv
|
|
23
|
+
// ---------------------------------------------------------------------
|
|
24
|
+
cli.parse();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ExtraExt } from "../../core";
|
|
2
|
+
import {
|
|
3
|
+
mergeMessages,
|
|
4
|
+
resolveLoaderOptions,
|
|
5
|
+
type IntorResolvedConfig,
|
|
6
|
+
type LocaleMessages,
|
|
7
|
+
type MessagesReaders,
|
|
8
|
+
} from "intor";
|
|
9
|
+
import { loadMessages } from "intor/server";
|
|
10
|
+
import { getBuiltInReaders } from "../collect-messages/readers";
|
|
11
|
+
import { resolveMessagesReader } from "./resolve-messages-reader";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Collect runtime messages for a given locale.
|
|
15
|
+
*
|
|
16
|
+
* Precedence: client-side runtime > server-side runtime > static messages.
|
|
17
|
+
*/
|
|
18
|
+
export async function collectRuntimeMessages(
|
|
19
|
+
config: IntorResolvedConfig,
|
|
20
|
+
locale: string,
|
|
21
|
+
exts: Array<ExtraExt> = [],
|
|
22
|
+
customReaders?: Record<string, string>, // {ext, customReaderFilePath}
|
|
23
|
+
): Promise<LocaleMessages> {
|
|
24
|
+
// ----------------------------------------------------------------------
|
|
25
|
+
// Resolve readers
|
|
26
|
+
// ----------------------------------------------------------------------
|
|
27
|
+
// Resolve optional custom readers
|
|
28
|
+
const resolvedCustomReaders: MessagesReaders = {};
|
|
29
|
+
for (const [ext, filePath] of Object.entries(customReaders || {})) {
|
|
30
|
+
const resolved = await resolveMessagesReader(filePath);
|
|
31
|
+
if (resolved) resolvedCustomReaders[ext] = resolved;
|
|
32
|
+
}
|
|
33
|
+
const readers = { ...getBuiltInReaders(exts), ...resolvedCustomReaders };
|
|
34
|
+
|
|
35
|
+
// ----------------------------------------------------------------------
|
|
36
|
+
// Load runtime messages
|
|
37
|
+
// ----------------------------------------------------------------------
|
|
38
|
+
let serverMessages: LocaleMessages | undefined;
|
|
39
|
+
let clientMessages: LocaleMessages | undefined;
|
|
40
|
+
|
|
41
|
+
// Load server-side runtime messages
|
|
42
|
+
if (config.loader || config.server?.loader) {
|
|
43
|
+
const serverLoader = resolveLoaderOptions(config, "server");
|
|
44
|
+
if (serverLoader) {
|
|
45
|
+
serverMessages = await loadMessages({
|
|
46
|
+
config: { ...config, loader: serverLoader },
|
|
47
|
+
locale,
|
|
48
|
+
readers,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Load client-side runtime messages
|
|
54
|
+
if (config.client?.loader) {
|
|
55
|
+
const clientLoader = resolveLoaderOptions(config, "client");
|
|
56
|
+
if (clientLoader) {
|
|
57
|
+
clientMessages = await loadMessages({
|
|
58
|
+
config: { ...config, loader: clientLoader },
|
|
59
|
+
locale,
|
|
60
|
+
readers,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ----------------------------------------------------------------------
|
|
66
|
+
// Merge messages
|
|
67
|
+
// ----------------------------------------------------------------------
|
|
68
|
+
// client-side runtime > server-side runtime
|
|
69
|
+
const runtimeMessages = mergeMessages(serverMessages, clientMessages, {
|
|
70
|
+
config,
|
|
71
|
+
locale,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// runtime messages > static messages
|
|
75
|
+
return mergeMessages(config.messages, runtimeMessages, { config, locale });
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { collectRuntimeMessages } from "./collect-runtime-messages";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ExtraExt } from "../../core/constants";
|
|
2
|
+
import type { MessagesReader, MessagesReaders } from "intor";
|
|
3
|
+
import { mdReader } from "@intor/reader-md";
|
|
4
|
+
import { yamlReader } from "@intor/reader-yaml";
|
|
5
|
+
|
|
6
|
+
export const BUILTIN_READERS: Record<ExtraExt, MessagesReader> = {
|
|
7
|
+
md: mdReader,
|
|
8
|
+
yaml: yamlReader,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const getBuiltInReaders = (
|
|
12
|
+
exts: readonly ExtraExt[],
|
|
13
|
+
): MessagesReaders => {
|
|
14
|
+
// Map to [ext, reader]
|
|
15
|
+
const entries = exts
|
|
16
|
+
.map((ext) => {
|
|
17
|
+
const reader = BUILTIN_READERS[ext];
|
|
18
|
+
return reader ? [ext, reader] : null;
|
|
19
|
+
})
|
|
20
|
+
.filter((e): e is [ExtraExt, MessagesReader] => e !== null);
|
|
21
|
+
// Entries to object
|
|
22
|
+
return Object.fromEntries(entries);
|
|
23
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { MessagesReader } from "intor";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a MessagesReader from a module path.
|
|
7
|
+
*
|
|
8
|
+
* Resolution order:
|
|
9
|
+
* 1. Default export
|
|
10
|
+
* 2. First exported function (fallback)
|
|
11
|
+
*/
|
|
12
|
+
export async function resolveMessagesReader(
|
|
13
|
+
filePath?: string,
|
|
14
|
+
): Promise<MessagesReader | undefined> {
|
|
15
|
+
if (!filePath) return;
|
|
16
|
+
|
|
17
|
+
const absolutePath = path.resolve(process.cwd(), filePath);
|
|
18
|
+
|
|
19
|
+
// ----------------------------------------------------------------------
|
|
20
|
+
// Ensure the file exists before attempting dynamic import
|
|
21
|
+
// ----------------------------------------------------------------------
|
|
22
|
+
try {
|
|
23
|
+
await fs.access(absolutePath);
|
|
24
|
+
} catch {
|
|
25
|
+
throw new Error(`[intor] Reader file not found: ${filePath}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ----------------------------------------------------------------------
|
|
29
|
+
// Resolve reader module exports
|
|
30
|
+
// ----------------------------------------------------------------------
|
|
31
|
+
// Dynamically import the reader module
|
|
32
|
+
const moduleExports = await import(absolutePath);
|
|
33
|
+
|
|
34
|
+
// Prefer default export
|
|
35
|
+
let reader = moduleExports.default;
|
|
36
|
+
|
|
37
|
+
// Fallback to the first exported function
|
|
38
|
+
if (typeof reader !== "function") {
|
|
39
|
+
for (const value of Object.values(moduleExports)) {
|
|
40
|
+
if (typeof value === "function") {
|
|
41
|
+
reader = value;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ----------------------------------------------------------------------
|
|
48
|
+
// Validate resolved reader
|
|
49
|
+
// ----------------------------------------------------------------------
|
|
50
|
+
if (typeof reader !== "function") {
|
|
51
|
+
throw new Error(
|
|
52
|
+
`[intor] No function export found in reader module: ${filePath}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return reader;
|
|
57
|
+
}
|