jui-tools-mcp-server 2.13.0 → 2.14.0
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/data/attribute_definitions.json +124 -74
- package/data/attribute_semantics.json +18 -1
- package/data/component_metadata.json +36 -7
- package/data/type_synonyms.json +167 -0
- package/dist/cli_runner.js +7 -1
- package/dist/spec_loader.d.ts +25 -0
- package/dist/spec_loader.js +71 -0
- package/dist/tools/jui/jui_generate_converter.js +10 -2
- package/dist/tools/jui/jui_generate_project.js +11 -2
- package/dist/tools/spec/get_data_source.js +5 -2
- package/package.json +1 -1
- package/scripts/fetch-definitions.js +1 -1
- package/scripts/fetch-manifest.js +6 -0
- package/src/cli_runner.ts +7 -1
- package/src/spec_loader.ts +85 -0
- package/src/tools/jui/jui_generate_converter.ts +10 -2
- package/src/tools/jui/jui_generate_project.ts +13 -2
- package/src/tools/spec/get_data_source.ts +5 -2
- package/tests/cli_runner.test.ts +38 -1
- package/tests/snapshot_completeness.test.ts +1 -0
- package/tests/staleness_is_content_not_mtime.test.ts +1 -1
- package/tests/tools_cli.test.ts +43 -0
- package/tests/tools_test.test.ts +4 -4
- package/tests/type_synonyms.test.ts +178 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type spellings the build accepts for a section they are not
|
|
3
|
+
* (jsonui-cli shared/core/type_synonyms.json, 1.9.0). Until then
|
|
4
|
+
* lookup_component answered "not found" for `ProgressBar`, which the build
|
|
5
|
+
* draws as a Progress: the server knew `_alias_of` sections and metadata
|
|
6
|
+
* aliases only (ticket mcp-lookup-component-does-not-know-type-synonyms).
|
|
7
|
+
*
|
|
8
|
+
* The loader must FOLLOW the file, not agree with it by coincidence: the
|
|
9
|
+
* synthetic canon below names spellings no real table has, and one arm edits
|
|
10
|
+
* the file under a running loader.
|
|
11
|
+
*/
|
|
12
|
+
import { writeFileSync } from "fs";
|
|
13
|
+
import { dirname, join, resolve } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
16
|
+
|
|
17
|
+
import { SpecLoader } from "../src/spec_loader.js";
|
|
18
|
+
import { register as registerLookupComponent } from "../src/tools/spec/lookup_component.js";
|
|
19
|
+
import { register as registerSearchComponents } from "../src/tools/spec/search_components.js";
|
|
20
|
+
import { register as registerDataSource } from "../src/tools/spec/get_data_source.js";
|
|
21
|
+
import {
|
|
22
|
+
cleanupTempDirs,
|
|
23
|
+
createToolHarness,
|
|
24
|
+
makeCliDataset,
|
|
25
|
+
makeTempDir,
|
|
26
|
+
writeJson,
|
|
27
|
+
FIXTURE_ATTRIBUTE_DEFINITIONS,
|
|
28
|
+
} from "./helpers.js";
|
|
29
|
+
|
|
30
|
+
const originalCwd = process.cwd();
|
|
31
|
+
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
32
|
+
|
|
33
|
+
const DEFINITIONS = {
|
|
34
|
+
...FIXTURE_ATTRIBUTE_DEFINITIONS,
|
|
35
|
+
View: { orientation: { type: "string", enum: ["horizontal", "vertical"], description: "Stack axis" } },
|
|
36
|
+
Image: { src: { type: "string", description: "Image source" } },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const SYNONYMS = {
|
|
40
|
+
_description: ["test fixture — spellings no real table has"],
|
|
41
|
+
synonyms: {
|
|
42
|
+
Tappable: { canonical: "Button" },
|
|
43
|
+
Lane: { canonical: "View", orientation: "horizontal" },
|
|
44
|
+
Roundel: { canonical: "Image", render_as: "CircleImage" },
|
|
45
|
+
// A section of its own (`_alias_of` Button) and a metadata alias of
|
|
46
|
+
// Label: the exact match is found first, as in the tools.
|
|
47
|
+
Flip: { canonical: "Label" },
|
|
48
|
+
Text: { canonical: "Button" },
|
|
49
|
+
// Its canonical is not a section here.
|
|
50
|
+
Ghost: { canonical: "NoSuchSection" },
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function checkout(synonyms: unknown | null): string {
|
|
55
|
+
const root = makeTempDir("cli");
|
|
56
|
+
makeCliDataset(root, { attributeDefinitions: DEFINITIONS });
|
|
57
|
+
if (synonyms !== null) writeJson(join(root, "shared", "core", "type_synonyms.json"), synonyms);
|
|
58
|
+
process.env.JSONUI_CLI_PATH = root;
|
|
59
|
+
return root;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
delete process.env.JSONUI_CLI_PATH;
|
|
64
|
+
const home = makeTempDir("home");
|
|
65
|
+
process.env.HOME = home;
|
|
66
|
+
process.env.USERPROFILE = home;
|
|
67
|
+
process.chdir(makeTempDir("cwd"));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
delete process.env.JSONUI_CLI_PATH;
|
|
72
|
+
process.chdir(originalCwd);
|
|
73
|
+
cleanupTempDirs();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("lookup through type_synonyms.json", () => {
|
|
77
|
+
it("resolves a synonym to its canonical section and says so", () => {
|
|
78
|
+
checkout(SYNONYMS);
|
|
79
|
+
const loader = new SpecLoader(makeTempDir("mcp-root"));
|
|
80
|
+
|
|
81
|
+
const tappable = loader.getComponentWithCommon("Tappable");
|
|
82
|
+
expect(tappable.name).toBe("Button");
|
|
83
|
+
expect(tappable.typeSynonym).toEqual({
|
|
84
|
+
spelling: "Tappable",
|
|
85
|
+
canonical: "Button",
|
|
86
|
+
source: "shared/core/type_synonyms.json",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(loader.getComponentWithCommon("lane").typeSynonym).toMatchObject({
|
|
90
|
+
canonical: "View",
|
|
91
|
+
implies: { orientation: "horizontal" },
|
|
92
|
+
});
|
|
93
|
+
expect(loader.getComponentWithCommon("Roundel")).toMatchObject({
|
|
94
|
+
name: "Image",
|
|
95
|
+
typeSynonym: { canonical: "Image", renderAs: "CircleImage" },
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("finds a section, an `_alias_of` section and a metadata alias first", () => {
|
|
100
|
+
checkout(SYNONYMS);
|
|
101
|
+
const loader = new SpecLoader(makeTempDir("mcp-root"));
|
|
102
|
+
|
|
103
|
+
expect(loader.getComponentWithCommon("Flip")).toMatchObject({ name: "Button" });
|
|
104
|
+
expect(loader.getComponentWithCommon("Flip").typeSynonym).toBeUndefined();
|
|
105
|
+
expect(loader.getComponentWithCommon("Text")).toMatchObject({ name: "Label" });
|
|
106
|
+
expect(loader.getComponentWithCommon("Text").typeSynonym).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("skips an entry whose canonical is not a section", () => {
|
|
110
|
+
checkout(SYNONYMS);
|
|
111
|
+
expect(new SpecLoader(makeTempDir("mcp-root")).getComponentWithCommon("Ghost")).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("knows no synonym without the file (jsonui-cli before 1.9.0)", () => {
|
|
115
|
+
checkout(null);
|
|
116
|
+
const loader = new SpecLoader(makeTempDir("mcp-root"));
|
|
117
|
+
expect(loader.getComponentWithCommon("Tappable")).toBeNull();
|
|
118
|
+
expect(loader.getDataSource().typeSynonyms).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("follows an edit to the file under a running server", () => {
|
|
122
|
+
const root = checkout(SYNONYMS);
|
|
123
|
+
const loader = new SpecLoader(makeTempDir("mcp-root"));
|
|
124
|
+
expect(loader.getComponentWithCommon("Pressable")).toBeNull();
|
|
125
|
+
|
|
126
|
+
const path = join(root, "shared", "core", "type_synonyms.json");
|
|
127
|
+
writeJson(path, { synonyms: { ...SYNONYMS.synonyms, Pressable: { canonical: "Button" } } });
|
|
128
|
+
expect(loader.refreshIfChanged()).toEqual([path]);
|
|
129
|
+
expect(loader.getComponentWithCommon("Pressable")).toMatchObject({ name: "Button" });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("keeps the last synonyms that parsed when the file is half-written", () => {
|
|
133
|
+
const root = checkout(SYNONYMS);
|
|
134
|
+
const loader = new SpecLoader(makeTempDir("mcp-root"));
|
|
135
|
+
|
|
136
|
+
writeFileSync(join(root, "shared", "core", "type_synonyms.json"), '{"synonyms": {"Tapp');
|
|
137
|
+
loader.refreshIfChanged();
|
|
138
|
+
expect(loader.getLastReload()).toMatchObject({ ok: false });
|
|
139
|
+
expect(loader.getComponentWithCommon("Tappable")).toMatchObject({ name: "Button" });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("matches a synonym's spelling in search_components", () => {
|
|
143
|
+
checkout(SYNONYMS);
|
|
144
|
+
const results = new SpecLoader(makeTempDir("mcp-root")).searchComponents("tappab");
|
|
145
|
+
expect(results).toContainEqual(
|
|
146
|
+
expect.objectContaining({ component: "Button", matches: ["type synonym: Tappable"] })
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("the tools", () => {
|
|
152
|
+
it("lookup_component answers a synonym, and get_data_source names the file", async () => {
|
|
153
|
+
const root = checkout(SYNONYMS);
|
|
154
|
+
const loader = new SpecLoader(makeTempDir("mcp-root"));
|
|
155
|
+
const harness = createToolHarness();
|
|
156
|
+
registerLookupComponent(harness.server, loader);
|
|
157
|
+
registerSearchComponents(harness.server, loader);
|
|
158
|
+
registerDataSource(harness.server, loader);
|
|
159
|
+
|
|
160
|
+
const found = JSON.parse(await harness.call("lookup_component", { name: "Lane" }));
|
|
161
|
+
expect(found.name).toBe("View");
|
|
162
|
+
expect(found.typeSynonym.implies).toEqual({ orientation: "horizontal" });
|
|
163
|
+
|
|
164
|
+
const source = JSON.parse(await harness.call("get_data_source"));
|
|
165
|
+
expect(source.typeSynonyms.path).toBe(join(root, "shared", "core", "type_synonyms.json"));
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// The measured case, on the committed snapshot: `ProgressBar` was "not found".
|
|
170
|
+
describe("the bundled snapshot", () => {
|
|
171
|
+
it("resolves ProgressBar, which the build accepts", () => {
|
|
172
|
+
const loader = new SpecLoader(repoRoot);
|
|
173
|
+
expect(loader.getDataSource().typeSynonyms?.layer).toBe("bundled");
|
|
174
|
+
const spec = loader.getComponentWithCommon("ProgressBar");
|
|
175
|
+
expect(spec).not.toBeNull();
|
|
176
|
+
expect(spec.typeSynonym).toMatchObject({ spelling: "ProgressBar", canonical: spec.name });
|
|
177
|
+
});
|
|
178
|
+
});
|