(code: string)
| 343 | |
| 344 | /** `null` when the code may be rendered, otherwise the reason to hand back. */ |
| 345 | export const validateArtifactCode = (code: string): string | null => { |
| 346 | // Checked first: a five-segment address is the mistake a model that only read |
| 347 | // the `execute` skill will make, and every other rejection below would be a |
| 348 | // confusing thing to hear about code whose real problem is its addressing. |
| 349 | const oldStyleAddress = oldStyleAddressRejection(code); |
| 350 | if (oldStyleAddress) return oldStyleAddress; |
| 351 | |
| 352 | if (REMOVED_RUN_CALL.test(code) && !LOCAL_RUN_DECLARATION.test(code)) { |
| 353 | return [ |
| 354 | "`run(code)` no longer exists — artifact code is purely declarative `tools.*`.", |
| 355 | "Read data with useQuery(tools.<ns>.<tool>.queryOptions(args)),", |
| 356 | "page through a cursor with useInfiniteQuery(tools.<ns>.<tool>.infiniteQueryOptions(args, { getNextPageParam })),", |
| 357 | "and write with useMutation(tools.<ns>.<tool>.mutationOptions({ onSuccess })).", |
| 358 | ].join(" "); |
| 359 | } |
| 360 | |
| 361 | const loopedHook = hookCalledInLoop(code); |
| 362 | if (loopedHook) { |
| 363 | return [ |
| 364 | `\`${loopedHook}\` is called inside a loop body.`, |
| 365 | "React hooks must run unconditionally at the top level of the component, in the same order every render.", |
| 366 | PAGINATION_HOOKS.has(loopedHook) |
| 367 | ? "To read every page of a cursor, use one useInfiniteQuery(tools.<ns>.<tool>.infiniteQueryOptions(args, { cursorKey, getNextPageParam })) and render data.pages — never a loop of useQuery calls, one per page." |
| 368 | : "Move the hook out of the loop, and derive per-item values from its result instead.", |
| 369 | ].join(" "); |
| 370 | } |
| 371 | |
| 372 | const missingProvider = providerPairingRejection(code); |
| 373 | if (missingProvider) return missingProvider; |
| 374 | |
| 375 | if (REACT_DESTRUCTURING_DECLARATION.test(code)) { |
| 376 | return [ |
| 377 | "Do not destructure React in create-artifact.", |
| 378 | "Hooks such as useState are already in scope; use useState(...) directly or React.useState(...).", |
| 379 | ].join(" "); |
| 380 | } |
| 381 | |
| 382 | for (const match of code.matchAll(OBJECT_DESTRUCTURING_DECLARATION)) { |
| 383 | const names = match[1]?.split(",").flatMap((part) => { |
| 384 | const name = localDestructuredName(part); |
| 385 | return name ? [name] : []; |
| 386 | }); |
| 387 | const providedName = names?.find((name) => PROVIDED_GLOBAL_NAMES.has(name)); |
| 388 | if (providedName) { |
| 389 | return [ |
| 390 | `Provided global "${providedName}" is already in scope and cannot be redeclared.`, |
| 391 | "Remove the destructuring declaration and use the provided global directly.", |
| 392 | ].join(" "); |
| 393 | } |
| 394 | } |
| 395 | |
| 396 | for (const match of code.matchAll(PROVIDED_GLOBAL_DECLARATION)) { |
| 397 | const name = firstDefined(match[1], match[2], match[3]); |
| 398 | if (name && PROVIDED_GLOBAL_NAMES.has(name)) { |
| 399 | return [ |
| 400 | `Provided global "${name}" is already in scope and cannot be redeclared.`, |
| 401 | "Remove the local declaration and use the provided global directly.", |
| 402 | ].join(" "); |
no test coverage detected