@assistant-ui/x-generative-compiler 0.0.2 → 0.0.4
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 +7 -5
- package/dist/compile.d.ts +1 -1
- package/dist/compile.d.ts.map +1 -1
- package/dist/compile.js +542 -51
- package/dist/compile.js.map +1 -1
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/package.json +10 -8
- package/src/compile.test.ts +841 -6
- package/src/compile.ts +957 -77
package/dist/compile.js
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import { DIRECTIVE } from "./constants.js";
|
|
2
|
+
import { version } from "./package.js";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import * as nodePath from "node:path";
|
|
2
6
|
import { parse } from "@babel/parser";
|
|
3
7
|
import _traverse from "@babel/traverse";
|
|
4
8
|
import _generate from "@babel/generator";
|
|
5
9
|
import * as t from "@babel/types";
|
|
10
|
+
import { satisfies } from "semver";
|
|
6
11
|
//#region src/compile.ts
|
|
7
12
|
const traverse = typeof _traverse === "function" ? _traverse : _traverse.default;
|
|
8
13
|
const generate = typeof _generate === "function" ? _generate : _generate.default;
|
|
14
|
+
/** The required wrapper around a toolkit's tools (stripped at build time). */
|
|
15
|
+
const TOOLKIT_WRAPPER = "defineToolkit";
|
|
16
|
+
/** The helper that produces MCP-only toolkit fragments. */
|
|
17
|
+
const MCP_TOOLKIT_WRAPPER = "defineMcpToolkit";
|
|
18
|
+
/** The required wrapper around a generative-UI library (stripped at build time). */
|
|
19
|
+
const COMPONENTS_WRAPPER = "defineGenerativeComponents";
|
|
20
|
+
/** The core package whose metadata declares supported compiler versions. */
|
|
21
|
+
const CORE_PACKAGE = "@assistant-ui/core";
|
|
22
|
+
/** This package, checked against core's compatibility range. */
|
|
23
|
+
const COMPILER_PACKAGE = "@assistant-ui/x-generative-compiler";
|
|
24
|
+
/** Packages that re-export core's generative markers. */
|
|
25
|
+
const DISTRIBUTION_PACKAGES = [
|
|
26
|
+
CORE_PACKAGE,
|
|
27
|
+
"@assistant-ui/react",
|
|
28
|
+
"@assistant-ui/react-native",
|
|
29
|
+
"@assistant-ui/react-ink"
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* The class whose instances expose split-by-condition tools (`present()`,
|
|
33
|
+
* `promptUser()`). A toolkit entry that calls a method on one of these passes
|
|
34
|
+
* through untouched — the library, not this compiler, routes its halves.
|
|
35
|
+
*/
|
|
36
|
+
const GENERATIVE_FACTORY = "JSONGenerativeUI";
|
|
9
37
|
/** Thrown when a `"use generative"` file violates an authoring constraint. */
|
|
10
38
|
var GenerativeCompileError = class extends Error {
|
|
11
39
|
constructor(message, filename) {
|
|
@@ -45,27 +73,32 @@ function compileGenerative(code, options) {
|
|
|
45
73
|
]
|
|
46
74
|
});
|
|
47
75
|
if (!ast.program.directives.some((d) => d.value.value === "use generative")) throw new GenerativeCompileError(`missing "${DIRECTIVE}" directive`, filename);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (execute && type === "backend") keptBackendExecute = true;
|
|
76
|
+
ensureCompilerCompatibleWithCore(ast, filename);
|
|
77
|
+
ensureDefaultExport(ast, filename);
|
|
78
|
+
const generativeInstances = collectGenerativeInstances(ast);
|
|
79
|
+
const safeToolkitSpreads = collectSafeToolkitSpreads(ast, filename);
|
|
80
|
+
const flags = {
|
|
81
|
+
keptRender: false,
|
|
82
|
+
keptBackendExecute: false
|
|
83
|
+
};
|
|
84
|
+
traverse(ast, { CallExpression(path) {
|
|
85
|
+
const callee = path.node.callee;
|
|
86
|
+
const object = t.isObjectExpression(path.node.arguments[0]) ? path.node.arguments[0] : null;
|
|
87
|
+
if (t.isIdentifier(callee, { name: COMPONENTS_WRAPPER })) {
|
|
88
|
+
if (!object) throw new GenerativeCompileError(`${COMPONENTS_WRAPPER}() takes an inline object literal of components`, filename);
|
|
89
|
+
compileComponents(object, target, flags, filename);
|
|
90
|
+
path.replaceWith(object);
|
|
91
|
+
path.skip();
|
|
92
|
+
return;
|
|
66
93
|
}
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
if (t.isIdentifier(callee, { name: TOOLKIT_WRAPPER })) {
|
|
95
|
+
if (!object) throw new GenerativeCompileError(`${TOOLKIT_WRAPPER}() takes an inline object literal of tools`, filename);
|
|
96
|
+
compileToolkit(object, target, generativeInstances, safeToolkitSpreads, flags, filename);
|
|
97
|
+
path.replaceWith(object);
|
|
98
|
+
path.skip();
|
|
99
|
+
}
|
|
100
|
+
} });
|
|
101
|
+
const { keptRender, keptBackendExecute } = flags;
|
|
69
102
|
pruneUnused(ast);
|
|
70
103
|
ast.program.directives = ast.program.directives.filter((d) => d.value.value !== "use generative" && d.value.value !== "use client");
|
|
71
104
|
if (target === "client" && keptRender) ast.program.directives.unshift(t.directive(t.directiveLiteral("use client")));
|
|
@@ -80,29 +113,453 @@ function compileGenerative(code, options) {
|
|
|
80
113
|
map: result.map
|
|
81
114
|
};
|
|
82
115
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
const checkedCorePackageJsonPaths = /* @__PURE__ */ new Set();
|
|
117
|
+
const COMPILER_VERSION = version;
|
|
118
|
+
function ensureCompilerCompatibleWithCore(ast, filename) {
|
|
119
|
+
const corePackageJsonPath = resolveCorePackageJson(ast, filename);
|
|
120
|
+
if (!corePackageJsonPath || checkedCorePackageJsonPaths.has(corePackageJsonPath)) return;
|
|
121
|
+
const corePackageJson = readPackageJson(corePackageJsonPath);
|
|
122
|
+
const range = corePackageJson?.optionalDevDependencies?.[COMPILER_PACKAGE];
|
|
123
|
+
if (!range) {
|
|
124
|
+
checkedCorePackageJsonPaths.add(corePackageJsonPath);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
let compatible = false;
|
|
128
|
+
try {
|
|
129
|
+
compatible = satisfies(COMPILER_VERSION, range, { includePrerelease: true });
|
|
130
|
+
} catch {
|
|
131
|
+
throw new GenerativeCompileError(`${CORE_PACKAGE}@${corePackageJson.version ?? "unknown"} declares an invalid optionalDevDependencies range for ${COMPILER_PACKAGE}: ` + JSON.stringify(range), filename);
|
|
132
|
+
}
|
|
133
|
+
if (!compatible) throw new GenerativeCompileError(`${CORE_PACKAGE}@${corePackageJson.version ?? "unknown"} requires ${COMPILER_PACKAGE} ${range}, but the current compiler is ${COMPILER_VERSION}. Update @assistant-ui/next, @assistant-ui/vite, or @assistant-ui/metro so their compiler satisfies the core package's optionalDevDependencies range.`, filename);
|
|
134
|
+
checkedCorePackageJsonPaths.add(corePackageJsonPath);
|
|
135
|
+
}
|
|
136
|
+
function resolveCorePackageJson(ast, filename) {
|
|
137
|
+
for (const packageName of collectImportedDistributionPackages(ast)) {
|
|
138
|
+
const packageJsonPath = resolvePackageJson(packageName, filename);
|
|
139
|
+
if (!packageJsonPath) continue;
|
|
140
|
+
if (packageName === CORE_PACKAGE) return packageJsonPath;
|
|
141
|
+
const corePackageJsonPath = resolvePackageJson(CORE_PACKAGE, packageJsonPath);
|
|
142
|
+
if (corePackageJsonPath) return corePackageJsonPath;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
function collectImportedDistributionPackages(ast) {
|
|
147
|
+
const packages = /* @__PURE__ */ new Set();
|
|
148
|
+
for (const statement of ast.program.body) {
|
|
149
|
+
const source = (t.isImportDeclaration(statement) || t.isExportNamedDeclaration(statement) || t.isExportAllDeclaration(statement)) && statement.source ? statement.source.value : null;
|
|
150
|
+
if (!source) continue;
|
|
151
|
+
const packageName = packageNameFromSpecifier(source);
|
|
152
|
+
if (packageName) packages.add(packageName);
|
|
153
|
+
}
|
|
154
|
+
return packages;
|
|
155
|
+
}
|
|
156
|
+
function packageNameFromSpecifier(specifier) {
|
|
157
|
+
for (const packageName of DISTRIBUTION_PACKAGES) if (specifier === packageName || specifier.startsWith(`${packageName}/`)) return packageName;
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function resolvePackageJson(packageName, filename) {
|
|
161
|
+
const requirePath = normalizeRequirePath(filename);
|
|
162
|
+
const require = createRequire(requirePath);
|
|
163
|
+
try {
|
|
164
|
+
return findPackageJson(require.resolve(packageName), packageName);
|
|
165
|
+
} catch {
|
|
166
|
+
return findPackageJsonFromNodeModules(packageName, requirePath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function normalizeRequirePath(filename) {
|
|
170
|
+
return cleanAbsoluteFilename(filename) ?? import.meta.url;
|
|
171
|
+
}
|
|
172
|
+
/** Strips a `?query`/`#hash` suffix and returns the path only if it is absolute. */
|
|
173
|
+
function cleanAbsoluteFilename(filename) {
|
|
174
|
+
if (!filename) return null;
|
|
175
|
+
const clean = filename.split(/[?#]/, 1)[0];
|
|
176
|
+
return nodePath.isAbsolute(clean) ? clean : null;
|
|
177
|
+
}
|
|
178
|
+
function findPackageJson(fromPathOrUrl, packageName) {
|
|
179
|
+
let current = nodePath.dirname(fromPathOrUrl.startsWith("file:") ? new URL(fromPathOrUrl).pathname : nodePath.resolve(fromPathOrUrl));
|
|
180
|
+
for (;;) {
|
|
181
|
+
const packageJsonPath = nodePath.join(current, "package.json");
|
|
182
|
+
if (readPackageJson(packageJsonPath)?.name === packageName) return packageJsonPath;
|
|
183
|
+
const parent = nodePath.dirname(current);
|
|
184
|
+
if (parent === current) return null;
|
|
185
|
+
current = parent;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function findPackageJsonFromNodeModules(packageName, fromPathOrUrl) {
|
|
189
|
+
const parts = packageName.split("/");
|
|
190
|
+
let current = nodePath.dirname(fromPathOrUrl.startsWith("file:") ? new URL(fromPathOrUrl).pathname : nodePath.resolve(fromPathOrUrl));
|
|
191
|
+
for (;;) {
|
|
192
|
+
const packageJsonPath = nodePath.join(current, "node_modules", ...parts, "package.json");
|
|
193
|
+
if (readPackageJson(packageJsonPath)?.name === packageName) return packageJsonPath;
|
|
194
|
+
const parent = nodePath.dirname(current);
|
|
195
|
+
if (parent === current) return null;
|
|
196
|
+
current = parent;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function readPackageJson(packageJsonPath) {
|
|
200
|
+
if (!existsSync(packageJsonPath)) return null;
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw new GenerativeCompileError(`could not parse package metadata at ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Errors unless the module's default export is the toolkit — a `defineToolkit(...)`
|
|
209
|
+
* call (through `satisfies`/`as`/parens). This is the security boundary: the
|
|
210
|
+
* default export is what the runtime registers, so it must be wrapped (and thus
|
|
211
|
+
* split). A bare `export default { ... }` would ship a backend `execute` to the
|
|
212
|
+
* client even if some *other* `defineToolkit(...)` exists elsewhere in the file.
|
|
213
|
+
*/
|
|
214
|
+
function ensureDefaultExport(ast, filename) {
|
|
215
|
+
const def = ast.program.body.find((stmt) => t.isExportDefaultDeclaration(stmt));
|
|
216
|
+
if (!def) throw new GenerativeCompileError("missing a default export", filename);
|
|
217
|
+
if (!unwrapToToolkitCall(def.declaration)) throw new GenerativeCompileError(`the default export must be ${TOOLKIT_WRAPPER}({ ... }) or ${MCP_TOOLKIT_WRAPPER}({ ... }) (imported from "@assistant-ui/react"); wrapping is required so a backend \`execute\` can't be authored in a way that reaches the client`, filename);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Unwraps a node through `satisfies`/`as`/parens to a call of the named function,
|
|
221
|
+
* or returns `null`.
|
|
222
|
+
*/
|
|
223
|
+
function unwrapToCall(node, name) {
|
|
224
|
+
if (t.isTSSatisfiesExpression(node) || t.isTSAsExpression(node)) return unwrapToCall(node.expression, name);
|
|
225
|
+
if (t.isParenthesizedExpression(node)) return unwrapToCall(node.expression, name);
|
|
226
|
+
if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name })) return node;
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Collects the names bound to `new JSONGenerativeUI(...)` (e.g.
|
|
231
|
+
* `const generative = new JSONGenerativeUI({ library })`). A toolkit entry that
|
|
232
|
+
* calls a method on one of these is a generative tool whose halves the library
|
|
233
|
+
* routes by export condition, so it passes through the toolkit pass untouched.
|
|
234
|
+
*/
|
|
235
|
+
function collectGenerativeInstances(ast) {
|
|
236
|
+
const names = /* @__PURE__ */ new Set();
|
|
237
|
+
for (const statement of ast.program.body) {
|
|
238
|
+
if (!t.isVariableDeclaration(statement)) continue;
|
|
239
|
+
for (const declaration of statement.declarations) {
|
|
240
|
+
const { id, init } = declaration;
|
|
241
|
+
if (t.isIdentifier(id) && t.isNewExpression(init) && t.isIdentifier(init.callee, { name: GENERATIVE_FACTORY })) names.add(id.name);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return names;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Toolkit identifiers that are safe to spread into a `defineToolkit({ ... })`.
|
|
248
|
+
*
|
|
249
|
+
* Two kinds qualify:
|
|
250
|
+
*
|
|
251
|
+
* - A local variable whose initializer is visible to this compiler pass: a
|
|
252
|
+
* `defineToolkit(...)` binding is compiled in-place before a later spread
|
|
253
|
+
* reads it, and `defineMcpToolkit(...)` entries can't contain executable code.
|
|
254
|
+
* - The default import of another `"use generative"` module: that module is
|
|
255
|
+
* split per-target by its own compiler pass, so spreading its default-exported
|
|
256
|
+
* toolkit can't leak a backend `execute` to the client. Only the default
|
|
257
|
+
* export crosses the generative-module boundary, so named imports don't
|
|
258
|
+
* qualify — they would be `undefined` once that module is build-split.
|
|
259
|
+
*/
|
|
260
|
+
function collectSafeToolkitSpreads(ast, filename) {
|
|
261
|
+
const names = /* @__PURE__ */ new Set();
|
|
262
|
+
const generativeBySource = /* @__PURE__ */ new Map();
|
|
263
|
+
for (const statement of ast.program.body) {
|
|
264
|
+
if (t.isVariableDeclaration(statement)) {
|
|
265
|
+
for (const declaration of statement.declarations) {
|
|
266
|
+
const { id, init } = declaration;
|
|
267
|
+
if (t.isIdentifier(id) && init && unwrapToToolkitCall(init)) names.add(id.name);
|
|
268
|
+
}
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (t.isImportDeclaration(statement)) {
|
|
272
|
+
const defaultSpecifier = statement.specifiers.find((specifier) => t.isImportDefaultSpecifier(specifier));
|
|
273
|
+
if (!defaultSpecifier) continue;
|
|
274
|
+
const source = statement.source.value;
|
|
275
|
+
let isGenerative = generativeBySource.get(source);
|
|
276
|
+
if (isGenerative === void 0) {
|
|
277
|
+
isGenerative = isGenerativeImport(source, filename);
|
|
278
|
+
generativeBySource.set(source, isGenerative);
|
|
279
|
+
}
|
|
280
|
+
if (isGenerative) names.add(defaultSpecifier.local.name);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return names;
|
|
284
|
+
}
|
|
285
|
+
const MODULE_EXTENSIONS = [
|
|
286
|
+
".ts",
|
|
287
|
+
".tsx",
|
|
288
|
+
".mts",
|
|
289
|
+
".cts",
|
|
290
|
+
".js",
|
|
291
|
+
".jsx",
|
|
292
|
+
".mjs",
|
|
293
|
+
".cjs"
|
|
294
|
+
];
|
|
295
|
+
/** Extensions a specifier may carry that actually map to a TS source file. */
|
|
296
|
+
const REWRITABLE_JS_EXTENSIONS = new Set([
|
|
297
|
+
".js",
|
|
298
|
+
".jsx",
|
|
299
|
+
".mjs",
|
|
300
|
+
".cjs"
|
|
301
|
+
]);
|
|
302
|
+
/**
|
|
303
|
+
* Whether an import specifier resolves on disk to a `"use generative"` module.
|
|
304
|
+
* Relative specifiers and `tsconfig` path aliases (e.g. `@/tools`) are resolved;
|
|
305
|
+
* anything else (a bare package, an unresolvable alias) is treated as
|
|
306
|
+
* non-generative, and thus an unsafe spread.
|
|
307
|
+
*/
|
|
308
|
+
function isGenerativeImport(source, filename) {
|
|
309
|
+
const cleanFilename = cleanAbsoluteFilename(filename);
|
|
310
|
+
if (!cleanFilename) return false;
|
|
311
|
+
const resolved = resolveImportedModuleFile(source, cleanFilename);
|
|
312
|
+
if (!resolved) return false;
|
|
313
|
+
try {
|
|
314
|
+
return isGenerativeModule(readFileSync(resolved, "utf8"));
|
|
315
|
+
} catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/** Resolves an import specifier (relative or `tsconfig`-aliased) to a file on disk. */
|
|
320
|
+
function resolveImportedModuleFile(source, fromFilename) {
|
|
321
|
+
if (source.startsWith(".")) return resolveModuleFileAtPath(nodePath.resolve(nodePath.dirname(fromFilename), source));
|
|
322
|
+
return resolveAliasImport(source, fromFilename);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Resolves a candidate module path, trying TS/JS extensions then index files. A
|
|
326
|
+
* specifier may carry a `.js`-family extension that maps to a `.ts`/`.tsx`
|
|
327
|
+
* source (TypeScript's `bundler`/`nodenext` resolution), so the extension is
|
|
328
|
+
* dropped before probing.
|
|
329
|
+
*/
|
|
330
|
+
function resolveModuleFileAtPath(base) {
|
|
331
|
+
const ext = nodePath.extname(base);
|
|
332
|
+
if (ext && existsSync(base)) return base;
|
|
333
|
+
const stem = REWRITABLE_JS_EXTENSIONS.has(ext) ? base.slice(0, -ext.length) : base;
|
|
334
|
+
for (const candidateExt of MODULE_EXTENSIONS) {
|
|
335
|
+
const candidate = `${stem}${candidateExt}`;
|
|
336
|
+
if (existsSync(candidate)) return candidate;
|
|
337
|
+
}
|
|
338
|
+
for (const candidateExt of MODULE_EXTENSIONS) {
|
|
339
|
+
const candidate = nodePath.join(base, `index${candidateExt}`);
|
|
340
|
+
if (existsSync(candidate)) return candidate;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
/** Resolves a `tsconfig` path alias (e.g. `@/tools/x`) to a file on disk. */
|
|
345
|
+
function resolveAliasImport(source, fromFilename) {
|
|
346
|
+
const tsconfig = loadTsconfigAliases(nodePath.dirname(fromFilename));
|
|
347
|
+
if (!tsconfig) return null;
|
|
348
|
+
const patterns = Object.entries(tsconfig.paths).sort(([a], [b]) => aliasSpecificity(b) - aliasSpecificity(a));
|
|
349
|
+
for (const [pattern, targets] of patterns) {
|
|
350
|
+
const matched = matchAliasPattern(pattern, source);
|
|
351
|
+
if (matched === null) continue;
|
|
352
|
+
for (const target of targets) {
|
|
353
|
+
const specifier = substituteAliasWildcard(target, matched);
|
|
354
|
+
const file = resolveModuleFileAtPath(nodePath.resolve(tsconfig.baseDir, specifier));
|
|
355
|
+
if (file) return file;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
/** Ranks a `paths` key: an exact key outranks any wildcard; longer prefixes win. */
|
|
361
|
+
function aliasSpecificity(pattern) {
|
|
362
|
+
const star = pattern.indexOf("*");
|
|
363
|
+
return star === -1 ? pattern.length + 1 : star;
|
|
364
|
+
}
|
|
365
|
+
/** Substitutes the single `*` in a `paths` target with the matched text. */
|
|
366
|
+
function substituteAliasWildcard(target, matched) {
|
|
367
|
+
const star = target.indexOf("*");
|
|
368
|
+
if (star === -1) return target;
|
|
369
|
+
return target.slice(0, star) + matched + target.slice(star + 1);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Matches an import specifier against a `tsconfig` `paths` key. Returns the text
|
|
373
|
+
* captured by the key's `*` (or `""` for an exact, wildcard-free key), or `null`
|
|
374
|
+
* when the specifier doesn't match.
|
|
375
|
+
*/
|
|
376
|
+
function matchAliasPattern(pattern, source) {
|
|
377
|
+
const star = pattern.indexOf("*");
|
|
378
|
+
if (star === -1) return pattern === source ? "" : null;
|
|
379
|
+
const prefix = pattern.slice(0, star);
|
|
380
|
+
const suffix = pattern.slice(star + 1);
|
|
381
|
+
if (source.length >= prefix.length + suffix.length && source.startsWith(prefix) && source.endsWith(suffix)) return source.slice(prefix.length, source.length - suffix.length);
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Memoizes resolved aliases per start directory. The compiler runs once per
|
|
386
|
+
* file across a build, so without this every aliased spread re-walks and
|
|
387
|
+
* re-parses the same `tsconfig.json`. Process-lifetime, like
|
|
388
|
+
* `checkedCorePackageJsonPaths`.
|
|
389
|
+
*/
|
|
390
|
+
const tsconfigAliasesByDir = /* @__PURE__ */ new Map();
|
|
391
|
+
/** Walks up from a directory to the nearest `tsconfig.json` that declares `paths`. */
|
|
392
|
+
function loadTsconfigAliases(fromDir) {
|
|
393
|
+
const cached = tsconfigAliasesByDir.get(fromDir);
|
|
394
|
+
if (cached !== void 0) return cached;
|
|
395
|
+
let aliases = null;
|
|
396
|
+
let dir = fromDir;
|
|
397
|
+
for (;;) {
|
|
398
|
+
const tsconfigPath = nodePath.join(dir, "tsconfig.json");
|
|
399
|
+
if (existsSync(tsconfigPath)) {
|
|
400
|
+
aliases = readTsconfigAliases(tsconfigPath, /* @__PURE__ */ new Set());
|
|
401
|
+
if (aliases) break;
|
|
402
|
+
}
|
|
403
|
+
const parent = nodePath.dirname(dir);
|
|
404
|
+
if (parent === dir) break;
|
|
405
|
+
dir = parent;
|
|
406
|
+
}
|
|
407
|
+
tsconfigAliasesByDir.set(fromDir, aliases);
|
|
408
|
+
return aliases;
|
|
409
|
+
}
|
|
410
|
+
/** Reads `baseUrl`/`paths` from a tsconfig, following a single `extends` chain. */
|
|
411
|
+
function readTsconfigAliases(tsconfigPath, seen) {
|
|
412
|
+
if (seen.has(tsconfigPath)) return null;
|
|
413
|
+
seen.add(tsconfigPath);
|
|
414
|
+
let config;
|
|
415
|
+
try {
|
|
416
|
+
config = parseJsonc(readFileSync(tsconfigPath, "utf8"));
|
|
417
|
+
} catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
if (!config) return null;
|
|
421
|
+
const configDir = nodePath.dirname(tsconfigPath);
|
|
422
|
+
const { baseUrl, paths } = config.compilerOptions ?? {};
|
|
423
|
+
if (paths) return {
|
|
424
|
+
baseDir: baseUrl ? nodePath.resolve(configDir, baseUrl) : configDir,
|
|
425
|
+
paths
|
|
426
|
+
};
|
|
427
|
+
const extendsList = Array.isArray(config.extends) ? config.extends : config.extends ? [config.extends] : [];
|
|
428
|
+
for (let i = extendsList.length - 1; i >= 0; i--) {
|
|
429
|
+
const entry = extendsList[i];
|
|
430
|
+
if (typeof entry !== "string") continue;
|
|
431
|
+
const extended = resolveExtendedTsconfig(entry, configDir);
|
|
432
|
+
if (extended) {
|
|
433
|
+
const aliases = readTsconfigAliases(extended, seen);
|
|
434
|
+
if (aliases) return aliases;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
/** Resolves a tsconfig `extends` value (relative path or installed package). */
|
|
440
|
+
function resolveExtendedTsconfig(extendsValue, configDir) {
|
|
441
|
+
if (extendsValue.startsWith(".")) {
|
|
442
|
+
const base = nodePath.resolve(configDir, extendsValue);
|
|
443
|
+
return (nodePath.extname(base) === ".json" ? [base] : [`${base}.json`, base]).find((candidate) => existsSync(candidate)) ?? null;
|
|
444
|
+
}
|
|
445
|
+
try {
|
|
446
|
+
return createRequire(nodePath.join(configDir, "package.json")).resolve(extendsValue.endsWith(".json") ? extendsValue : `${extendsValue}.json`);
|
|
447
|
+
} catch {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/** Parses JSON with `//` and block comments and trailing commas stripped (tsconfig is JSONC). */
|
|
452
|
+
function parseJsonc(text) {
|
|
453
|
+
const withoutTrailingCommas = text.replace(/"(?:[^"\\]|\\.)*"|\/\/[^\n\r]*|\/\*[\s\S]*?\*\//g, (match) => match.startsWith("\"") ? match : "").replace(/,(\s*[}\]])/g, "$1");
|
|
454
|
+
return JSON.parse(withoutTrailingCommas);
|
|
455
|
+
}
|
|
456
|
+
function unwrapToToolkitCall(node) {
|
|
457
|
+
return unwrapToCall(node, TOOLKIT_WRAPPER) ?? unwrapToCall(node, MCP_TOOLKIT_WRAPPER);
|
|
458
|
+
}
|
|
459
|
+
function isSafeToolkitSpread(entry, safeToolkitSpreads) {
|
|
460
|
+
if (t.isIdentifier(entry.argument)) return safeToolkitSpreads.has(entry.argument.name);
|
|
461
|
+
const directMcpToolkit = unwrapToCall(entry.argument, MCP_TOOLKIT_WRAPPER);
|
|
462
|
+
return !!directMcpToolkit && t.isObjectExpression(directMcpToolkit.arguments[0]);
|
|
463
|
+
}
|
|
464
|
+
/** The `JSONGenerativeUI` methods that produce a split-by-condition tool. */
|
|
465
|
+
const GENERATIVE_TOOL_METHODS = new Set(["present", "promptUser"]);
|
|
466
|
+
/**
|
|
467
|
+
* Whether a toolkit entry's value is a call to a tool-producing method on a
|
|
468
|
+
* collected `JSONGenerativeUI` instance (`generative.present()`), which passes
|
|
469
|
+
* through. The method name is checked too, so a typo like `generative.presnt()`
|
|
470
|
+
* is a compile error here rather than a pass-through that fails at runtime.
|
|
471
|
+
*/
|
|
472
|
+
function isGenerativeToolEntry(value, instances) {
|
|
473
|
+
return t.isCallExpression(value) && t.isMemberExpression(value.callee) && !value.callee.computed && t.isIdentifier(value.callee.object) && instances.has(value.callee.object.name) && t.isIdentifier(value.callee.property) && GENERATIVE_TOOL_METHODS.has(value.callee.property.name);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Splits a `defineGenerativeComponents({ ... })` library for a build target:
|
|
477
|
+
* a component's `render` (and the client imports it alone uses) is dropped on
|
|
478
|
+
* the server; `properties`/`description` stay on both, since they drive the tool
|
|
479
|
+
* schema either way. Mutates the object in place.
|
|
480
|
+
*/
|
|
481
|
+
function compileComponents(object, target, flags, filename) {
|
|
482
|
+
for (const entry of object.properties) {
|
|
483
|
+
const value = entryValue(entry);
|
|
484
|
+
if (!value) throw new GenerativeCompileError(`each component in ${COMPONENTS_WRAPPER}() must be an inline object literal (\`name: { ... }\`) so its \`render\` can be routed`, filename);
|
|
485
|
+
if (!findMember(value, "render")) continue;
|
|
486
|
+
if (target === "client") flags.keptRender = true;
|
|
487
|
+
else removeMember(value, "render");
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Splits a `defineToolkit({ ... })` for a build target. Each inline tool is
|
|
492
|
+
* routed by inferred type (see the per-entry logic); a generative entry like
|
|
493
|
+
* `generative.present()` passes through, the library having already split it.
|
|
494
|
+
* Mutates the object in place and records outcomes in {@link TargetFlags}.
|
|
495
|
+
*/
|
|
496
|
+
function compileToolkit(object, target, instances, safeToolkitSpreads, flags, filename) {
|
|
497
|
+
const nextProperties = [];
|
|
498
|
+
for (const entry of object.properties) {
|
|
499
|
+
const value = entryValue(entry);
|
|
500
|
+
if (!value) {
|
|
501
|
+
if (t.isSpreadElement(entry) && isSafeToolkitSpread(entry, safeToolkitSpreads)) {
|
|
502
|
+
nextProperties.push(entry);
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
const raw = entryRawValue(entry);
|
|
506
|
+
if (raw && isGenerativeToolEntry(raw, instances)) {
|
|
507
|
+
nextProperties.push(entry);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
throw new GenerativeCompileError("each tool must be an inline object literal (`name: { ... }`) or a compiler-visible toolkit spread / generative tool (e.g. `...defineMcpToolkit(...)`, `...baseToolkit`, or `generative.present()`) so its `execute` can be routed", filename);
|
|
511
|
+
}
|
|
512
|
+
const execute = findMember(value, "execute");
|
|
513
|
+
const isStub = execute ? executeIsStubTool(execute) : false;
|
|
514
|
+
const isExternal = execute ? executeIsExternalTool(execute) : false;
|
|
515
|
+
const type = inferToolType(value, filename);
|
|
516
|
+
const hasRender = !!findMember(value, "render");
|
|
517
|
+
const hasRenderText = !!findMember(value, "renderText");
|
|
518
|
+
if (type === "frontend" && !hasRender && !hasRenderText) throw new GenerativeCompileError("a frontend tool must declare a `render` or `renderText` (it has no server execute to show otherwise)", filename);
|
|
519
|
+
if (type === "human" && !hasRender) throw new GenerativeCompileError("a human tool must declare a `render` so it can collect input", filename);
|
|
520
|
+
if (type === "provider" && execute) applyProviderToolConfig(value, execute, filename);
|
|
521
|
+
if (isExternal) {
|
|
522
|
+
if (!hasRender && !hasRenderText) throw new GenerativeCompileError("an external tool must declare a `render` or `renderText` (assistant-ui only renders calls for tools defined elsewhere)", filename);
|
|
523
|
+
if (target === "server") continue;
|
|
524
|
+
stripExternalToolMetadata(value);
|
|
525
|
+
}
|
|
526
|
+
if (target === "client") {
|
|
527
|
+
if (execute && type === "frontend" && !isStub) stripUseClient(execute);
|
|
528
|
+
else if (execute) removeMember(value, "execute");
|
|
529
|
+
if (hasRender || hasRenderText) flags.keptRender = true;
|
|
530
|
+
} else {
|
|
531
|
+
if (hasRender) removeMember(value, "render");
|
|
532
|
+
if (hasRenderText) removeMember(value, "renderText");
|
|
533
|
+
if (execute) if (type === "backend" && !isExternal) flags.keptBackendExecute = true;
|
|
534
|
+
else removeMember(value, "execute");
|
|
535
|
+
}
|
|
536
|
+
setToolType(value, type);
|
|
537
|
+
setBackendDefault(value, target, type);
|
|
538
|
+
nextProperties.push(entry);
|
|
539
|
+
}
|
|
540
|
+
object.properties = nextProperties;
|
|
541
|
+
}
|
|
542
|
+
function applyProviderToolConfig(object, execute, filename) {
|
|
543
|
+
if (!t.isObjectProperty(execute) || !t.isCallExpression(execute.value) || execute.value.arguments.length !== 1 || !t.isObjectExpression(execute.value.arguments[0])) throw new GenerativeCompileError("`providerTool(...)` must receive an inline object literal", filename);
|
|
544
|
+
const existingNames = new Set(object.properties.flatMap((prop) => {
|
|
545
|
+
if (!t.isObjectProperty(prop) && !t.isObjectMethod(prop)) return [];
|
|
546
|
+
const name = memberName(prop.key, prop.computed);
|
|
547
|
+
return name ? [name] : [];
|
|
548
|
+
}));
|
|
549
|
+
const configNames = /* @__PURE__ */ new Set();
|
|
550
|
+
for (const prop of execute.value.arguments[0].properties) {
|
|
551
|
+
if (!t.isObjectProperty(prop)) throw new GenerativeCompileError("`providerTool(...)` config can only contain object properties", filename);
|
|
552
|
+
const name = memberName(prop.key, prop.computed);
|
|
553
|
+
if (!name) throw new GenerativeCompileError("`providerTool(...)` config can only contain static property names", filename);
|
|
554
|
+
if (t.isFunctionExpression(prop.value) || t.isArrowFunctionExpression(prop.value)) throw new GenerativeCompileError("`providerTool(...)` config cannot contain function-valued properties", filename);
|
|
555
|
+
if (existingNames.has(name) || configNames.has(name)) throw new GenerativeCompileError("`providerTool(...)` config cannot duplicate tool properties", filename);
|
|
556
|
+
configNames.add(name);
|
|
557
|
+
object.properties.push(prop);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/** The raw AST value of an entry (any expression), or null for spreads/methods. */
|
|
561
|
+
function entryRawValue(entry) {
|
|
562
|
+
if (t.isObjectProperty(entry) && t.isExpression(entry.value)) return entry.value;
|
|
106
563
|
return null;
|
|
107
564
|
}
|
|
108
565
|
function entryValue(entry) {
|
|
@@ -126,9 +583,24 @@ function executeBody(member) {
|
|
|
126
583
|
function executeIsClient(member) {
|
|
127
584
|
return !!executeBody(member)?.directives.some((d) => d.value.value === "use client");
|
|
128
585
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
586
|
+
function executeIsSentinel(member, name) {
|
|
587
|
+
return t.isObjectProperty(member) && t.isCallExpression(member.value) && t.isIdentifier(member.value.callee, { name });
|
|
588
|
+
}
|
|
589
|
+
/** Whether an `execute` is the human-in-the-loop sentinel. */
|
|
590
|
+
function executeIsHumanTool(member) {
|
|
591
|
+
return executeIsSentinel(member, "humanTool") || executeIsSentinel(member, "hitlTool") || executeIsSentinel(member, "hitl");
|
|
592
|
+
}
|
|
593
|
+
/** Whether an `execute` is the provider-tool sentinel. */
|
|
594
|
+
function executeIsProviderTool(member) {
|
|
595
|
+
return executeIsSentinel(member, "providerTool");
|
|
596
|
+
}
|
|
597
|
+
/** Whether an `execute` is the local override sentinel. */
|
|
598
|
+
function executeIsStubTool(member) {
|
|
599
|
+
return executeIsSentinel(member, "stubTool");
|
|
600
|
+
}
|
|
601
|
+
/** Whether an `execute` is the externally-defined backend tool sentinel. */
|
|
602
|
+
function executeIsExternalTool(member) {
|
|
603
|
+
return executeIsSentinel(member, "externalTool");
|
|
132
604
|
}
|
|
133
605
|
/** Drops the `"use client"` directive from an `execute` body (kept frontend). */
|
|
134
606
|
function stripUseClient(member) {
|
|
@@ -137,21 +609,40 @@ function stripUseClient(member) {
|
|
|
137
609
|
}
|
|
138
610
|
/**
|
|
139
611
|
* The tool's nature, inferred from its (mandatory) `execute` rather than an
|
|
140
|
-
* authored `type`: `
|
|
141
|
-
* `
|
|
142
|
-
*
|
|
612
|
+
* authored `type`: `humanTool()` → `human`; `providerTool(...)` → `provider`;
|
|
613
|
+
* `stubTool()` → `frontend`; `externalTool()` → `backend`; `"use client"` →
|
|
614
|
+
* `frontend`; otherwise `backend`.
|
|
615
|
+
* The loader writes the result back as a `type` field (see {@link setToolType})
|
|
616
|
+
* so the runtime keeps it.
|
|
143
617
|
*/
|
|
144
618
|
function inferToolType(object, filename) {
|
|
145
619
|
const execute = findMember(object, "execute");
|
|
146
|
-
if (!execute) throw new GenerativeCompileError("every tool must declare an `execute`; use `
|
|
147
|
-
if (
|
|
620
|
+
if (!execute) throw new GenerativeCompileError("every tool must declare an `execute`; use `humanTool()` for a human-in-the-loop tool", filename);
|
|
621
|
+
if (executeIsHumanTool(execute)) return "human";
|
|
622
|
+
if (executeIsProviderTool(execute)) return "provider";
|
|
623
|
+
if (executeIsStubTool(execute)) return "frontend";
|
|
624
|
+
if (executeIsExternalTool(execute)) return "backend";
|
|
148
625
|
return executeIsClient(execute) ? "frontend" : "backend";
|
|
149
626
|
}
|
|
627
|
+
function stripExternalToolMetadata(object) {
|
|
628
|
+
removeMember(object, "description");
|
|
629
|
+
removeMember(object, "parameters");
|
|
630
|
+
removeMember(object, "disabled");
|
|
631
|
+
removeMember(object, "toModelOutput");
|
|
632
|
+
removeMember(object, "experimental_onSchemaValidationError");
|
|
633
|
+
removeMember(object, "providerOptions");
|
|
634
|
+
removeMember(object, "streamCall");
|
|
635
|
+
}
|
|
150
636
|
/** Writes the resolved `type` back onto the tool object (replacing any author's). */
|
|
151
637
|
function setToolType(object, type) {
|
|
152
638
|
removeMember(object, "type");
|
|
153
639
|
object.properties.push(t.objectProperty(t.identifier("type"), t.stringLiteral(type)));
|
|
154
640
|
}
|
|
641
|
+
function setBackendDefault(object, target, type) {
|
|
642
|
+
removeMember(object, "unstable_backendDefault");
|
|
643
|
+
if (target !== "client" || type !== "frontend" && type !== "human") return;
|
|
644
|
+
object.properties.push(t.objectProperty(t.identifier("unstable_backendDefault"), t.objectExpression([t.objectProperty(t.identifier("parameters"), t.booleanLiteral(true))])));
|
|
645
|
+
}
|
|
155
646
|
function memberName(key, computed) {
|
|
156
647
|
if (computed) return void 0;
|
|
157
648
|
if (t.isIdentifier(key)) return key.name;
|