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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intor-cli",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "📟 CLI tool for intor",
5
5
  "author": "Yiming Liao",
6
6
  "homepage": "https://github.com/yiming-liao/intor-cli#readme",
@@ -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(input.schemas.richSchema);
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
- return "Record<string, unknown>";
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({ exts, customReaders, debug });
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
- const { messages, overrides } = await collectRuntimeMessages(
57
- config,
58
- config.defaultLocale,
59
- exts,
60
- customReaders,
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]);