@grampro/kit 2.7.2 → 2.7.3
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/.gbs/skills/gbs-components/SKILL.md +233 -209
- package/.gbs/skills/gbs-components/references/data-grid.md +9 -0
- package/.gbs/skills/gbs-components/references/forms.md +12 -0
- package/.gbs/skills/gbs-components/references/install.md +14 -3
- package/CHANGELOG.md +963 -861
- package/README.md +11 -3
- package/index.cjs +108 -123
- package/package.json +1 -1
- package/source/components/accordion/passport.json +1 -1
- package/source/components/alert/passport.json +1 -1
- package/source/components/avatar/passport.json +1 -1
- package/source/components/badge/passport.json +1 -1
- package/source/components/breadcrumb/passport.json +1 -1
- package/source/components/button/passport.json +1 -1
- package/source/components/card/passport.json +1 -1
- package/source/components/checkbox/passport.json +2 -2
- package/source/components/checkbox/passport.manual.json +1 -1
- package/source/components/checkbox/react/Checkbox.tsx +14 -0
- package/source/components/combobox/passport.json +3 -2
- package/source/components/combobox/passport.manual.json +1 -1
- package/source/components/combobox/react/MultiSelect.tsx +13 -0
- package/source/components/combobox/react/Select.tsx +13 -0
- package/source/components/combobox/react/surface.ts +70 -0
- package/source/components/data-grid/passport.json +6 -6
- package/source/components/data-grid/react/DataGrid.tsx +353 -351
- package/source/components/date-picker/passport.json +3 -2
- package/source/components/date-picker/passport.manual.json +1 -1
- package/source/components/date-picker/react/DatePicker.tsx +16 -2
- package/source/components/date-picker/react/DateRangePicker.tsx +16 -2
- package/source/components/date-picker/react/surface.ts +68 -0
- package/source/components/dialog/passport.json +1 -1
- package/source/components/file-uploader/passport.json +1 -1
- package/source/components/input/passport.json +2 -2
- package/source/components/input/passport.manual.json +1 -1
- package/source/components/input/react/Input.tsx +21 -0
- package/source/components/menu/passport.json +1 -1
- package/source/components/modal/passport.json +1 -1
- package/source/components/number-input/passport.json +2 -2
- package/source/components/number-input/passport.manual.json +1 -1
- package/source/components/number-input/react/NumberInput.tsx +30 -0
- package/source/components/popover/passport.json +1 -1
- package/source/components/progress/passport.json +1 -1
- package/source/components/radio-group/passport.json +2 -2
- package/source/components/radio-group/passport.manual.json +1 -1
- package/source/components/radio-group/react/RadioGroup.tsx +33 -0
- package/source/components/shared/core/agent/field-executors.ts +173 -137
- package/source/components/shared/core/agent/index.ts +21 -0
- package/source/components/shared/core/agent/surface.ts +437 -0
- package/source/components/shared/core/agent/webmcp.ts +332 -328
- package/source/components/shared/core/agent/write.ts +62 -58
- package/source/components/shared/index.ts +7 -0
- package/source/components/shared/react/AgentSurface.tsx +145 -0
- package/source/components/shared/react/agentSurfaceContext.ts +92 -0
- package/source/components/shared/react/useFieldAgent.ts +62 -88
- package/source/components/shared/version.json +4 -4
- package/source/components/shared/version.ts +6 -6
- package/source/components/skeleton/passport.json +1 -1
- package/source/components/spinner/passport.json +1 -1
- package/source/components/switch/passport.json +2 -2
- package/source/components/switch/passport.manual.json +1 -1
- package/source/components/switch/react/Switch.tsx +16 -0
- package/source/components/tabs/passport.json +1 -1
- package/source/components/textarea/passport.json +2 -2
- package/source/components/textarea/passport.manual.json +1 -1
- package/source/components/textarea/react/Textarea.tsx +16 -0
- package/source/components/toaster/passport.json +1 -1
- package/source/components/tooltip/passport.json +1 -1
- package/tools/doctor/baseline.cjs +5 -4
- package/tools/mcp/server.cjs +13 -1
- package/tools/passport/extract.cjs +20 -65
- package/tools/passport/hash.cjs +18 -1
- package/tools/passport/index.cjs +4 -2
- package/tools/passport/typescript.cjs +220 -0
- package/tools/prompt.cjs +205 -0
- package/tools/ui.cjs +67 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"ActionIntent",
|
|
7
7
|
"Summarization"
|
|
8
8
|
],
|
|
9
|
-
"$operationsNote": "Mirrors shared/core/agent/field.ts, which is what the runtime dispatches on. source/components/shared/core/agent/__tests__/field-passport.test.ts fails if the two drift. No `apiMethod`: a field is written through its ref — the element for most of these, a handle for Combobox and DatePicker — and that is a normal case, not a gap.",
|
|
9
|
+
"$operationsNote": "Mirrors shared/core/agent/field.ts, which is what the runtime dispatches on. source/components/shared/core/agent/__tests__/field-passport.test.ts fails if the two drift. No `apiMethod`: a field is written through its ref — the element for most of these, a handle for Combobox and DatePicker — and that is a normal case, not a gap. Inside an <AgentSurface> the component registers itself with these operations; NumberInput and RadioGroup are then written through their own commit path rather than the element.",
|
|
10
10
|
"operations": {
|
|
11
11
|
"setValue": {
|
|
12
12
|
"summary": "Write a value into the field, as though a person had entered it.",
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "react";
|
|
17
17
|
import { useControllableState } from "../../shared/react/useControllableState";
|
|
18
18
|
import { describeField } from "../../shared/core/field";
|
|
19
|
+
import { labelText, useAgentSurfaceField } from "../../shared/react/agentSurfaceContext";
|
|
19
20
|
import { countCharacters } from "../core/count";
|
|
20
21
|
import { fitHeight } from "../core/size";
|
|
21
22
|
import type { FieldSize, TextareaLocaleText, TextareaResize } from "../core/types";
|
|
@@ -99,6 +100,21 @@ export function Textarea(props: TextareaProps) {
|
|
|
99
100
|
|
|
100
101
|
// Mirrors the text even when uncontrolled, for the counter and auto-resize.
|
|
101
102
|
const [current, setCurrent] = useControllableState(valueProp, defaultValue ?? "");
|
|
103
|
+
|
|
104
|
+
// Inside an <AgentSurface>, offered to an agent with what it accepts now.
|
|
105
|
+
useAgentSurfaceField(() => ({
|
|
106
|
+
contract: {
|
|
107
|
+
component: "Textarea",
|
|
108
|
+
kind: "text",
|
|
109
|
+
label: labelText(label, textareaProps["aria-label"], textareaProps.name, textareaProps.placeholder),
|
|
110
|
+
disabled,
|
|
111
|
+
readOnly,
|
|
112
|
+
required,
|
|
113
|
+
maxLength,
|
|
114
|
+
operations: ["setValue", "clear", "focus"],
|
|
115
|
+
},
|
|
116
|
+
target: () => textareaRef.current,
|
|
117
|
+
}));
|
|
102
118
|
const floor = Math.max(1, minRows ?? rows ?? 3);
|
|
103
119
|
|
|
104
120
|
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
const fs = require("fs");
|
|
22
22
|
const path = require("path");
|
|
23
23
|
const crypto = require("crypto");
|
|
24
|
-
const {
|
|
24
|
+
const { componentHash } = require("../passport/hash.cjs");
|
|
25
25
|
const { compactContract } = require("./contract.cjs");
|
|
26
26
|
|
|
27
27
|
const MANIFEST = ".install-manifest.json";
|
|
@@ -100,12 +100,13 @@ function kitRelativeHash(dir, folder, passport) {
|
|
|
100
100
|
if (typeof relative !== "string" || relative.split(/[\\/]/).includes("..")) return null;
|
|
101
101
|
const file = path.join(dir, relative);
|
|
102
102
|
if (!fs.existsSync(file)) return null; // a file the passport lists is gone: not pristine
|
|
103
|
-
entries.push({
|
|
103
|
+
entries.push({ rel: relative, file });
|
|
104
104
|
}
|
|
105
105
|
// The generator hashes styles.css alongside the source when there is one.
|
|
106
106
|
const styles = path.join(dir, "styles.css");
|
|
107
|
-
if (fs.existsSync(styles)) entries.push({
|
|
108
|
-
|
|
107
|
+
if (fs.existsSync(styles)) entries.push({ rel: "styles.css", file: styles });
|
|
108
|
+
// The generator's own labelling rule, not a copy of it.
|
|
109
|
+
return componentHash(folder, entries);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
/**
|
package/tools/mcp/server.cjs
CHANGED
|
@@ -308,6 +308,11 @@ function installComponents(names) {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
const cli = path.join(__dirname, "..", "..", "index.cjs");
|
|
311
|
+
const withoutColour = (env) => {
|
|
312
|
+
const copy = { ...env };
|
|
313
|
+
delete copy.FORCE_COLOR;
|
|
314
|
+
return copy;
|
|
315
|
+
};
|
|
311
316
|
let stdout = "";
|
|
312
317
|
let stderr = "";
|
|
313
318
|
let failed = false;
|
|
@@ -319,6 +324,9 @@ function installComponents(names) {
|
|
|
319
324
|
// Captured, not forwarded: the installer explains a refusal on stderr,
|
|
320
325
|
// and that explanation is what the agent most needs to read.
|
|
321
326
|
stdio: ["ignore", "pipe", "pipe"],
|
|
327
|
+
// The output below is read by pattern. Colour would break the match, and
|
|
328
|
+
// a host that sets FORCE_COLOR for its own reasons would turn it on.
|
|
329
|
+
env: { ...withoutColour(process.env), NO_COLOR: "1" },
|
|
322
330
|
});
|
|
323
331
|
} catch (error) {
|
|
324
332
|
failed = true;
|
|
@@ -326,6 +334,10 @@ function installComponents(names) {
|
|
|
326
334
|
stderr = (error.stderr || "").toString();
|
|
327
335
|
if (!stdout && !stderr) return { error: `Install failed: ${error.message}` };
|
|
328
336
|
}
|
|
337
|
+
// Belt and braces: whatever reached the output, match it without escapes.
|
|
338
|
+
const ansi = /\x1b\[[0-9;?]*[A-Za-z]/g;
|
|
339
|
+
stdout = stdout.replace(ansi, "");
|
|
340
|
+
stderr = stderr.replace(ansi, "");
|
|
329
341
|
|
|
330
342
|
// The installer prints the one manual step; the agent needs to see it.
|
|
331
343
|
const importLine = (stdout.match(/^\s*@import .+$/gm) || []).map((l) => l.trim());
|
|
@@ -395,7 +407,7 @@ const TOOLS = [
|
|
|
395
407
|
{
|
|
396
408
|
name: "get_guidance",
|
|
397
409
|
description:
|
|
398
|
-
"Authored guidance on a topic: forms, overlays, data-grid, pickers, styling, accessibility, data-display, toaster, install. Omit the topic to list them.",
|
|
410
|
+
"Authored guidance on a topic: forms (including making a page operable by a browser agent with <AgentSurface>), overlays, data-grid, pickers, styling, accessibility, data-display, toaster, install. Omit the topic to list them.",
|
|
399
411
|
inputSchema: {
|
|
400
412
|
type: "object",
|
|
401
413
|
properties: { topic: { type: "string" } },
|
|
@@ -16,7 +16,8 @@ const fs = require("fs");
|
|
|
16
16
|
const path = require("path");
|
|
17
17
|
const { allowlistFor } = require("./policy.cjs");
|
|
18
18
|
const { describable } = require("./merge.cjs");
|
|
19
|
-
const {
|
|
19
|
+
const { componentHash } = require("./hash.cjs");
|
|
20
|
+
const { resolveTypeScript } = require("./typescript.cjs");
|
|
20
21
|
|
|
21
22
|
const PASSPORT_VERSION = "1.0.0";
|
|
22
23
|
|
|
@@ -28,55 +29,16 @@ const PASSPORT_VERSION = "1.0.0";
|
|
|
28
29
|
* consumer regenerating a forked component uses their own TypeScript.
|
|
29
30
|
*/
|
|
30
31
|
/*
|
|
31
|
-
* TypeScript
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* the first enum it is asked for, with an error about `ReactJSX` that says
|
|
36
|
-
* nothing useful.
|
|
37
|
-
*
|
|
38
|
-
* So a candidate is only accepted if it actually has the API. After the
|
|
39
|
-
* project's own `typescript`, a TypeScript 6 installed under an alias is
|
|
40
|
-
* tried: it sits beside a TypeScript 7 build without replacing it.
|
|
32
|
+
* A TypeScript with the compiler API, on any project: the project's own when
|
|
33
|
+
* it has the API, otherwise the kit's pinned version, fetched once and
|
|
34
|
+
* cached. See typescript.cjs for why, and for how the fetch is kept narrow.
|
|
35
|
+
* Progress goes to stderr so `doctor --json` keeps a clean stdout.
|
|
41
36
|
*/
|
|
42
|
-
const TS6_ALIASES = ["typescript-6", "typescript6"];
|
|
43
|
-
const hasCompilerApi = (ts) => Boolean(ts && typeof ts.createProgram === "function" && ts.JsxEmit);
|
|
44
|
-
|
|
45
37
|
function loadTypeScript(searchFrom = []) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
} catch {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
const primary = tryLoad("typescript");
|
|
56
|
-
if (hasCompilerApi(primary)) return primary;
|
|
57
|
-
for (const alias of TS6_ALIASES) {
|
|
58
|
-
const candidate = tryLoad(alias);
|
|
59
|
-
if (hasCompilerApi(candidate)) return candidate;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (primary) {
|
|
63
|
-
const error = new Error(
|
|
64
|
-
`This project's TypeScript (${primary.version || "unknown version"}) does not provide the compiler API that ` +
|
|
65
|
-
"passports are read with: TypeScript 7 is a native rewrite without it.\n" +
|
|
66
|
-
" Install TypeScript 6 alongside it, under another name, so your build keeps using 7:\n" +
|
|
67
|
-
" npm i -D typescript-6@npm:typescript@6\n" +
|
|
68
|
-
" It is only used to read component contracts, never to build or run a component.",
|
|
69
|
-
);
|
|
70
|
-
error.code = "ETYPESCRIPT_API";
|
|
71
|
-
throw error;
|
|
72
|
-
}
|
|
73
|
-
const error = new Error(
|
|
74
|
-
"Passport generation needs the TypeScript compiler API.\n" +
|
|
75
|
-
" Install it as a dev dependency: npm i -D typescript-6@npm:typescript@6\n" +
|
|
76
|
-
" It is only needed to read component contracts, never to run a component.",
|
|
77
|
-
);
|
|
78
|
-
error.code = "ETYPESCRIPT_MISSING";
|
|
79
|
-
throw error;
|
|
38
|
+
return resolveTypeScript({
|
|
39
|
+
searchFrom,
|
|
40
|
+
log: (message) => process.stderr.write(`${message}\n`),
|
|
41
|
+
}).ts;
|
|
80
42
|
}
|
|
81
43
|
|
|
82
44
|
const posix = (p) => p.replace(/\\/g, "/");
|
|
@@ -96,20 +58,6 @@ function sourceFiles(dir) {
|
|
|
96
58
|
return out.sort();
|
|
97
59
|
}
|
|
98
60
|
|
|
99
|
-
/**
|
|
100
|
-
* Hash of the inputs the passport is derived from. Deterministic: sorted paths,
|
|
101
|
-
* file contents only, no timestamps.
|
|
102
|
-
*
|
|
103
|
-
* Line endings are normalised first. The same file arrives as CRLF on a
|
|
104
|
-
* Windows checkout and LF on CI, so hashing the bytes verbatim made a
|
|
105
|
-
* passport generated on one platform read as stale on the other — which it
|
|
106
|
-
* did, for all 27 components at once. A content hash should not depend on
|
|
107
|
-
* how the file was checked out.
|
|
108
|
-
*/
|
|
109
|
-
function hashSources(files, root) {
|
|
110
|
-
// One implementation, shared with the installer: see hash.cjs for why.
|
|
111
|
-
return hashEntries(files.map((file) => ({ label: posix(path.relative(root, file)), file })));
|
|
112
|
-
}
|
|
113
61
|
|
|
114
62
|
/* ------------------------------------------------------------------ program */
|
|
115
63
|
|
|
@@ -476,9 +424,17 @@ function extractComponent(ts, program, checker, { name, dir, libRoot, libraryVer
|
|
|
476
424
|
exports: componentExports.sort(),
|
|
477
425
|
},
|
|
478
426
|
source: {
|
|
479
|
-
|
|
427
|
+
// A project regenerating its copies stamps each with the kit version it
|
|
428
|
+
// came from, not the project's own version, so `passport --check` can
|
|
429
|
+
// compare like with like.
|
|
430
|
+
libraryVersion: typeof libraryVersion === "function" ? libraryVersion(name, dir) : libraryVersion,
|
|
480
431
|
files: files.map((f) => posix(path.relative(dir, f))),
|
|
481
|
-
sourceHash:
|
|
432
|
+
sourceHash: componentHash(
|
|
433
|
+
name,
|
|
434
|
+
[...files, ...(fs.existsSync(path.join(dir, "styles.css")) ? [path.join(dir, "styles.css")] : [])].map(
|
|
435
|
+
(file) => ({ rel: posix(path.relative(dir, file)), file }),
|
|
436
|
+
),
|
|
437
|
+
),
|
|
482
438
|
},
|
|
483
439
|
props: propList,
|
|
484
440
|
...(externalBases.size
|
|
@@ -525,6 +481,5 @@ module.exports = {
|
|
|
525
481
|
createProgram,
|
|
526
482
|
extractComponent,
|
|
527
483
|
sourceFiles,
|
|
528
|
-
hashSources,
|
|
529
484
|
posix,
|
|
530
485
|
};
|
package/tools/passport/hash.cjs
CHANGED
|
@@ -36,4 +36,21 @@ function hashEntries(entries) {
|
|
|
36
36
|
return `sha256:${h.digest("hex")}`;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
/**
|
|
40
|
+
* A component's sourceHash, labelled the same way wherever the component sits.
|
|
41
|
+
*
|
|
42
|
+
* Every file is labelled `source/components/<folder>/<path in folder>` — the
|
|
43
|
+
* path it has in this repository. In the repository that is simply where it
|
|
44
|
+
* is; in a project it is installed under `component-lib/` or `src/...`, and
|
|
45
|
+
* labelling it by that location made an untouched installed component hash
|
|
46
|
+
* differently from the passport it shipped with. One rule for both, so the
|
|
47
|
+
* generator, `passport --check` in a project, and `doctor` all agree.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} folder e.g. "date-picker"
|
|
50
|
+
* @param {{ rel: string, file: string }[]} files path inside the folder ("/"-separated), absolute file
|
|
51
|
+
*/
|
|
52
|
+
function componentHash(folder, files) {
|
|
53
|
+
return hashEntries(files.map(({ rel, file }) => ({ label: `source/components/${folder}/${rel}`, file })));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { hashEntries, componentHash };
|
package/tools/passport/index.cjs
CHANGED
|
@@ -149,6 +149,8 @@ function generate(opts) {
|
|
|
149
149
|
*/
|
|
150
150
|
function check(opts) {
|
|
151
151
|
const { results } = buildAll(opts);
|
|
152
|
+
// In this repository it is an npm script; in a project, the CLI.
|
|
153
|
+
const regenerate = opts.regenerate || "npm run passport";
|
|
152
154
|
const report = [];
|
|
153
155
|
let errors = 0;
|
|
154
156
|
let warnings = 0;
|
|
@@ -161,14 +163,14 @@ function check(opts) {
|
|
|
161
163
|
const problems = [...r.issues];
|
|
162
164
|
if (actual === null) {
|
|
163
165
|
problems.unshift({ level: "error", code: "missing-passport", component: r.name,
|
|
164
|
-
message:
|
|
166
|
+
message: `no passport.json committed; run \`${regenerate}\`` });
|
|
165
167
|
} else if (actual !== expected) {
|
|
166
168
|
const reason =
|
|
167
169
|
JSON.parse(actual).source?.sourceHash !== r.effective.source.sourceHash
|
|
168
170
|
? "source changed since the passport was generated"
|
|
169
171
|
: "committed passport differs from generated output";
|
|
170
172
|
problems.unshift({ level: "error", code: "stale-passport", component: r.name,
|
|
171
|
-
message: `passport stale — ${reason}; run
|
|
173
|
+
message: `passport stale — ${reason}; run \`${regenerate}\`` });
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
errors += problems.filter((p) => p.level === "error").length;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A TypeScript that has the compiler API, whatever the project has installed.
|
|
3
|
+
*
|
|
4
|
+
* Reading a component's contract means resolving types — following
|
|
5
|
+
* `ButtonVariant` to its string literals, expanding
|
|
6
|
+
* `Omit<ButtonHTMLAttributes<…>, "onClick">` into the props it leaves — and
|
|
7
|
+
* that is the type checker's job. The kit has always borrowed the project's
|
|
8
|
+
* TypeScript for it rather than shipping one, so that `npx @grampro/kit add`
|
|
9
|
+
* stays a small download.
|
|
10
|
+
*
|
|
11
|
+
* TypeScript 7 broke the borrowing. It is a native rewrite, its npm package
|
|
12
|
+
* does not expose the JavaScript compiler API (`createProgram` and the rest
|
|
13
|
+
* are absent), and it is what `npm i -D typescript` installs now. So when the
|
|
14
|
+
* project's TypeScript cannot do the job, the exact version the kit itself is
|
|
15
|
+
* built and tested with is fetched once, cached per user, and used from then
|
|
16
|
+
* on. Same extractor, same result, on any TypeScript the project happens to
|
|
17
|
+
* use — or none.
|
|
18
|
+
*
|
|
19
|
+
* Fetching code at run time is a supply-chain surface, so it is narrow:
|
|
20
|
+
* - one exact version, never a range, so what runs is what was tested;
|
|
21
|
+
* - installed by the npm the user already runs `npx` with, so the registry,
|
|
22
|
+
* proxy, mirror and integrity checks are theirs, unchanged;
|
|
23
|
+
* - `--ignore-scripts`, so nothing executes during the install;
|
|
24
|
+
* - written to a temporary folder and moved into place only once complete,
|
|
25
|
+
* so an interrupted download never leaves a half-cache that loads;
|
|
26
|
+
* - checked after install: the version must be the pinned one and the API
|
|
27
|
+
* must be present, or it is not used;
|
|
28
|
+
* - `GRAMPRO_KIT_OFFLINE=1`, or npm's own offline setting, turns it off, and
|
|
29
|
+
* the callers fall back to what needs no TypeScript.
|
|
30
|
+
* Nothing about the project changes: no package.json edit, no node_modules.
|
|
31
|
+
*/
|
|
32
|
+
const fs = require("fs");
|
|
33
|
+
const os = require("os");
|
|
34
|
+
const path = require("path");
|
|
35
|
+
const { execFileSync } = require("child_process");
|
|
36
|
+
|
|
37
|
+
/** The kit's own TypeScript. A test fails if this drifts from the devDependency. */
|
|
38
|
+
const PINNED_VERSION = "6.0.3";
|
|
39
|
+
|
|
40
|
+
/** Older names a project may have installed TypeScript 6 under, to sit beside 7. */
|
|
41
|
+
const ALIASES = ["typescript-6", "typescript6"];
|
|
42
|
+
|
|
43
|
+
const hasCompilerApi = (ts) => Boolean(ts && typeof ts.createProgram === "function" && ts.JsxEmit);
|
|
44
|
+
|
|
45
|
+
function tryRequire(name, paths) {
|
|
46
|
+
try {
|
|
47
|
+
return require(require.resolve(name, { paths }));
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The per-user cache, where the operating system expects caches to live. */
|
|
54
|
+
function cacheRoot(env = process.env, platform = process.platform, home = os.homedir()) {
|
|
55
|
+
if (env.GRAMPRO_KIT_CACHE_DIR) return path.resolve(env.GRAMPRO_KIT_CACHE_DIR);
|
|
56
|
+
if (platform === "win32") {
|
|
57
|
+
return path.join(env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "grampro-kit", "Cache");
|
|
58
|
+
}
|
|
59
|
+
if (platform === "darwin") return path.join(home, "Library", "Caches", "grampro-kit");
|
|
60
|
+
return path.join(env.XDG_CACHE_HOME || path.join(home, ".cache"), "grampro-kit");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cacheDirFor = (version = PINNED_VERSION, env = process.env) =>
|
|
64
|
+
path.join(cacheRoot(env), `typescript-${version}`);
|
|
65
|
+
|
|
66
|
+
/** The pinned TypeScript, if a previous run already fetched it. */
|
|
67
|
+
function fromCache(dir) {
|
|
68
|
+
const pkg = path.join(dir, "node_modules", "typescript", "package.json");
|
|
69
|
+
try {
|
|
70
|
+
if (JSON.parse(fs.readFileSync(pkg, "utf8")).version !== PINNED_VERSION) return null;
|
|
71
|
+
const ts = require(path.join(dir, "node_modules", "typescript"));
|
|
72
|
+
return hasCompilerApi(ts) ? ts : null;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* The npm CLI's own entry point, run with the current node — never through a
|
|
80
|
+
* shell. `npm_execpath` is set when this is running under npm or npx; failing
|
|
81
|
+
* that, npm sits next to node in every standard install.
|
|
82
|
+
*/
|
|
83
|
+
function findNpmCli(env = process.env, execPath = process.execPath) {
|
|
84
|
+
const candidates = [];
|
|
85
|
+
if (env.npm_execpath) {
|
|
86
|
+
const given = env.npm_execpath;
|
|
87
|
+
candidates.push(given.replace(/npx-cli\.js$/, "npm-cli.js"), given);
|
|
88
|
+
}
|
|
89
|
+
const bin = path.dirname(execPath);
|
|
90
|
+
candidates.push(
|
|
91
|
+
path.join(bin, "node_modules", "npm", "bin", "npm-cli.js"), // Windows
|
|
92
|
+
path.join(bin, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"), // macOS, Linux
|
|
93
|
+
);
|
|
94
|
+
return candidates.find((file) => /npm-cli\.js$/.test(file) && fs.existsSync(file)) || null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const offline = (env) =>
|
|
98
|
+
env.GRAMPRO_KIT_OFFLINE === "1" || String(env.npm_config_offline).toLowerCase() === "true";
|
|
99
|
+
|
|
100
|
+
/** Fetch the pinned version into the cache. Returns the loaded module, or throws. */
|
|
101
|
+
function download({ env = process.env, log = () => {} } = {}) {
|
|
102
|
+
const npmCli = findNpmCli(env);
|
|
103
|
+
if (!npmCli) {
|
|
104
|
+
const error = new Error("npm was not found next to node, so TypeScript could not be fetched.");
|
|
105
|
+
error.code = "ETYPESCRIPT_FETCH";
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const dir = cacheDirFor(PINNED_VERSION, env);
|
|
110
|
+
const staging = `${dir}.partial-${process.pid}`;
|
|
111
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
112
|
+
fs.mkdirSync(staging, { recursive: true });
|
|
113
|
+
// A minimal manifest so npm treats the folder as its own project, not the user's.
|
|
114
|
+
fs.writeFileSync(path.join(staging, "package.json"), '{"name":"grampro-kit-cache","private":true}\n');
|
|
115
|
+
|
|
116
|
+
log(
|
|
117
|
+
`Fetching TypeScript ${PINNED_VERSION} once, to read component contracts ` +
|
|
118
|
+
`(cached in ${path.dirname(dir)}).`,
|
|
119
|
+
);
|
|
120
|
+
try {
|
|
121
|
+
execFileSync(
|
|
122
|
+
process.execPath,
|
|
123
|
+
[
|
|
124
|
+
npmCli,
|
|
125
|
+
"install",
|
|
126
|
+
`typescript@${PINNED_VERSION}`,
|
|
127
|
+
"--prefix",
|
|
128
|
+
staging,
|
|
129
|
+
"--no-save",
|
|
130
|
+
"--no-package-lock",
|
|
131
|
+
"--ignore-scripts",
|
|
132
|
+
"--no-audit",
|
|
133
|
+
"--no-fund",
|
|
134
|
+
"--loglevel=error",
|
|
135
|
+
],
|
|
136
|
+
{ stdio: ["ignore", "ignore", "pipe"], timeout: 180000, env },
|
|
137
|
+
);
|
|
138
|
+
} catch (cause) {
|
|
139
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
140
|
+
const detail = String(cause.stderr || cause.message || "").trim().split("\n").slice(-2).join(" ");
|
|
141
|
+
const error = new Error(`Could not fetch TypeScript ${PINNED_VERSION}: ${detail || "npm failed"}`);
|
|
142
|
+
error.code = "ETYPESCRIPT_FETCH";
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Verified before it is trusted, then moved into place in one step.
|
|
147
|
+
const fetched = fromCache(staging);
|
|
148
|
+
if (!fetched) {
|
|
149
|
+
fs.rmSync(staging, { recursive: true, force: true });
|
|
150
|
+
const error = new Error(`The fetched TypeScript was not version ${PINNED_VERSION} with the compiler API.`);
|
|
151
|
+
error.code = "ETYPESCRIPT_FETCH";
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
156
|
+
fs.renameSync(staging, dir);
|
|
157
|
+
} catch {
|
|
158
|
+
// Another run finished first, or the rename is not allowed here; the
|
|
159
|
+
// verified staging copy is still fine to use for this run.
|
|
160
|
+
return fetched;
|
|
161
|
+
}
|
|
162
|
+
return fromCache(dir) || fetched;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The first TypeScript that has the compiler API: the project's own, a
|
|
167
|
+
* TypeScript 6 the project installed under an alias, the cached copy, or a
|
|
168
|
+
* freshly fetched one.
|
|
169
|
+
*
|
|
170
|
+
* @returns {{ ts: object, source: "project" | "alias" | "cache" | "downloaded" }}
|
|
171
|
+
* @throws ETYPESCRIPT_API when the project's TypeScript lacks the API and
|
|
172
|
+
* fetching is turned off or fails; ETYPESCRIPT_MISSING when there is
|
|
173
|
+
* no TypeScript at all and fetching is off or fails.
|
|
174
|
+
*/
|
|
175
|
+
function resolveTypeScript({ searchFrom = [], env = process.env, allowDownload = true, log = () => {} } = {}) {
|
|
176
|
+
const paths = [...searchFrom, process.cwd(), __dirname].filter(Boolean);
|
|
177
|
+
|
|
178
|
+
const project = tryRequire("typescript", paths);
|
|
179
|
+
if (hasCompilerApi(project)) return { ts: project, source: "project" };
|
|
180
|
+
for (const alias of ALIASES) {
|
|
181
|
+
const candidate = tryRequire(alias, paths);
|
|
182
|
+
if (hasCompilerApi(candidate)) return { ts: candidate, source: "alias" };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const cached = fromCache(cacheDirFor(PINNED_VERSION, env));
|
|
186
|
+
if (cached) return { ts: cached, source: "cache" };
|
|
187
|
+
|
|
188
|
+
let fetchError = null;
|
|
189
|
+
if (allowDownload && !offline(env)) {
|
|
190
|
+
try {
|
|
191
|
+
return { ts: download({ env, log }), source: "downloaded" };
|
|
192
|
+
} catch (error) {
|
|
193
|
+
fetchError = error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const why = fetchError
|
|
198
|
+
? ` ${fetchError.message}`
|
|
199
|
+
: offline(env)
|
|
200
|
+
? " Fetching it is turned off (offline)."
|
|
201
|
+
: "";
|
|
202
|
+
const error = new Error(
|
|
203
|
+
(project
|
|
204
|
+
? `This project's TypeScript (${project.version || "unknown"}) has no compiler API — TypeScript 7 is a native rewrite without it — and TypeScript ${PINNED_VERSION} could not be used instead.`
|
|
205
|
+
: `Reading component contracts needs TypeScript ${PINNED_VERSION}, which could not be used.`) +
|
|
206
|
+
why +
|
|
207
|
+
` It is fetched automatically when online; to provide it yourself: npm i -D typescript-6@npm:typescript@${PINNED_VERSION}`,
|
|
208
|
+
);
|
|
209
|
+
error.code = project ? "ETYPESCRIPT_API" : "ETYPESCRIPT_MISSING";
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = {
|
|
214
|
+
PINNED_VERSION,
|
|
215
|
+
resolveTypeScript,
|
|
216
|
+
hasCompilerApi,
|
|
217
|
+
cacheRoot,
|
|
218
|
+
cacheDirFor,
|
|
219
|
+
findNpmCli,
|
|
220
|
+
};
|