intor-cli 0.0.10 → 0.0.12
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/package.json +1 -1
- package/src/build/build-types/build-types.ts +4 -1
- package/src/build/build-types/utils/normalize-rich-infer-node.ts +40 -0
- package/src/build/build-types/utils/render-infer-node.ts +3 -1
- package/src/cli/commands/generate.ts +12 -2
- package/src/features/generate/generate.ts +22 -6
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BuildInput } from "../types";
|
|
2
2
|
import { appendHeader, appendConfigBlock, appendFooter } from "./output";
|
|
3
|
+
import { normalizeRichInferNode } from "./utils/normalize-rich-infer-node";
|
|
3
4
|
import { renderInferNode } from "./utils/render-infer-node";
|
|
4
5
|
|
|
5
6
|
const GENERATED_INTERFACE_NAME = "IntorGeneratedTypes";
|
|
@@ -18,7 +19,9 @@ export function buildTypes(inputs: BuildInput[]): string {
|
|
|
18
19
|
const localesType = input.locales.map((l) => `"${l}"`).join(" | ");
|
|
19
20
|
const messagesType = renderInferNode(input.schemas.messagesSchema);
|
|
20
21
|
const replacementsType = renderInferNode(input.schemas.replacementsSchema);
|
|
21
|
-
const richType = renderInferNode(
|
|
22
|
+
const richType = renderInferNode(
|
|
23
|
+
normalizeRichInferNode(input.schemas.richSchema),
|
|
24
|
+
);
|
|
22
25
|
|
|
23
26
|
if (index === 0) {
|
|
24
27
|
appendConfigBlock(lines, {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { InferNode } from "../../../core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalize inferred rich schema before type emission.
|
|
5
|
+
*
|
|
6
|
+
* Rich schema normalization rules:
|
|
7
|
+
*
|
|
8
|
+
* - `none` means: the tag exists but has no nested structure
|
|
9
|
+
* → normalize to `record` so it emits `Record<string, never>`
|
|
10
|
+
* → this preserves tag presence while preventing arbitrary indexing
|
|
11
|
+
*
|
|
12
|
+
* - `object` nodes are recursively normalized
|
|
13
|
+
*
|
|
14
|
+
* This normalization is required to keep rich tag autocompletion working.
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeRichInferNode(node: InferNode): InferNode {
|
|
17
|
+
// Leaf rich tag: presence marker only
|
|
18
|
+
if (node.kind === "none") {
|
|
19
|
+
return {
|
|
20
|
+
kind: "record",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Recursively normalize nested rich tag trees
|
|
25
|
+
if (node.kind === "object") {
|
|
26
|
+
return {
|
|
27
|
+
kind: "object",
|
|
28
|
+
properties: Object.fromEntries(
|
|
29
|
+
Object.entries(node.properties).map(([key, child]) => [
|
|
30
|
+
key,
|
|
31
|
+
normalizeRichInferNode(child),
|
|
32
|
+
]),
|
|
33
|
+
),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Other kinds should not appear in rich schema,
|
|
38
|
+
// but are passed through defensively
|
|
39
|
+
return node;
|
|
40
|
+
}
|
|
@@ -13,7 +13,9 @@ export function renderInferNode(node: InferNode, indentLevel = 4): string {
|
|
|
13
13
|
return `${renderInferNode(node.element, indentLevel)}[]`;
|
|
14
14
|
}
|
|
15
15
|
case "record": {
|
|
16
|
-
|
|
16
|
+
// Rich leaf nodes must be non-indexable to preserve
|
|
17
|
+
// key narrowing and autocomplete behavior
|
|
18
|
+
return "Record<string, never>";
|
|
17
19
|
}
|
|
18
20
|
case "object": {
|
|
19
21
|
return `{
|
|
@@ -13,6 +13,10 @@ export function registerGenerateCommand(cli: CAC) {
|
|
|
13
13
|
// -----------------------------------------------------------------------
|
|
14
14
|
// Option
|
|
15
15
|
// -----------------------------------------------------------------------
|
|
16
|
+
.option(
|
|
17
|
+
"--messages <path>",
|
|
18
|
+
"Explicit messages file for schema generation (bypass runtime loader)",
|
|
19
|
+
)
|
|
16
20
|
.option(
|
|
17
21
|
"--ext <ext>",
|
|
18
22
|
"Enable extra messages file extension (repeatable)",
|
|
@@ -32,7 +36,8 @@ export function registerGenerateCommand(cli: CAC) {
|
|
|
32
36
|
// Action
|
|
33
37
|
// -----------------------------------------------------------------------
|
|
34
38
|
.action(async (options) => {
|
|
35
|
-
const { ext, reader, debug } = options as {
|
|
39
|
+
const { messages, ext, reader, debug } = options as {
|
|
40
|
+
messages?: string;
|
|
36
41
|
ext?: Array<ExtraExt>;
|
|
37
42
|
reader?: string[];
|
|
38
43
|
debug?: boolean;
|
|
@@ -41,7 +46,12 @@ export function registerGenerateCommand(cli: CAC) {
|
|
|
41
46
|
const { exts, customReaders } = normalizeReaderOptions({ ext, reader });
|
|
42
47
|
|
|
43
48
|
try {
|
|
44
|
-
await generate({
|
|
49
|
+
await generate({
|
|
50
|
+
messageFilePath: messages,
|
|
51
|
+
exts,
|
|
52
|
+
customReaders,
|
|
53
|
+
debug,
|
|
54
|
+
});
|
|
45
55
|
} catch (error) {
|
|
46
56
|
console.error(error);
|
|
47
57
|
process.exitCode = 1;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/* eslint-disable unicorn/no-process-exit */
|
|
2
2
|
import type { ExtraExt } from "../../core";
|
|
3
|
+
import type { MergeOverrides } from "../../core/collect-messages/types";
|
|
4
|
+
import type { LocaleMessages } from "intor";
|
|
5
|
+
import { readFile } from "node:fs/promises";
|
|
3
6
|
import path from "node:path";
|
|
4
7
|
import { buildTypes, buildSchemas, type BuildInput } from "../../build";
|
|
5
8
|
import {
|
|
@@ -16,12 +19,14 @@ import { printOverrides } from "./print-overrides";
|
|
|
16
19
|
import { printSummary } from "./print-summary";
|
|
17
20
|
|
|
18
21
|
export interface GenerateOptions {
|
|
22
|
+
messageFilePath?: string;
|
|
19
23
|
exts?: Array<ExtraExt>;
|
|
20
24
|
customReaders?: Record<string, string>;
|
|
21
25
|
debug?: boolean;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export async function generate({
|
|
29
|
+
messageFilePath,
|
|
25
30
|
exts = [],
|
|
26
31
|
customReaders,
|
|
27
32
|
debug,
|
|
@@ -30,6 +35,8 @@ export async function generate({
|
|
|
30
35
|
spinner.start();
|
|
31
36
|
const start = performance.now();
|
|
32
37
|
|
|
38
|
+
const hasTarget = !!messageFilePath;
|
|
39
|
+
|
|
33
40
|
try {
|
|
34
41
|
// -----------------------------------------------------------------------
|
|
35
42
|
// Discover configs from the current workspace
|
|
@@ -53,12 +60,21 @@ export async function generate({
|
|
|
53
60
|
print(`${dim("Config:")} ${cyan(config.id)} ${dim(`⚲ ${filePath}`)}`);
|
|
54
61
|
spinner.start();
|
|
55
62
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
63
|
+
let messages: LocaleMessages;
|
|
64
|
+
let overrides: MergeOverrides[] = [];
|
|
65
|
+
if (!hasTarget) {
|
|
66
|
+
const result = await collectRuntimeMessages(
|
|
67
|
+
config,
|
|
68
|
+
config.defaultLocale,
|
|
69
|
+
exts,
|
|
70
|
+
customReaders,
|
|
71
|
+
);
|
|
72
|
+
messages = result.messages;
|
|
73
|
+
overrides = result.overrides;
|
|
74
|
+
} else {
|
|
75
|
+
const content = await readFile(messageFilePath, "utf8");
|
|
76
|
+
messages = { [config.defaultLocale]: JSON.parse(content) };
|
|
77
|
+
}
|
|
62
78
|
|
|
63
79
|
printOverrides(overrides);
|
|
64
80
|
const schemas = inferSchemas(messages[config.defaultLocale]);
|