(code: string)
| 425 | |
| 426 | /** `null` when the code may be rendered, otherwise the reason to hand back. */ |
| 427 | export const validateArtifactCode = (code: string): string | null => { |
| 428 | // Checked first: a five-segment address is the mistake a model that only read |
| 429 | // the `execute` skill will make, and every other rejection below would be a |
| 430 | // confusing thing to hear about code whose real problem is its addressing. |
| 431 | const oldStyleAddress = oldStyleAddressRejection(code); |
| 432 | if (oldStyleAddress) return oldStyleAddress; |
| 433 | |
| 434 | if (REMOVED_RUN_CALL.test(code) && !LOCAL_RUN_DECLARATION.test(code)) { |
| 435 | return [ |
| 436 | "`run(code)` no longer exists — artifact code is purely declarative `tools.*`.", |
| 437 | "Read data with useQuery(tools.<ns>.<tool>.queryOptions(args)),", |
| 438 | "page through a cursor with useInfiniteQuery(tools.<ns>.<tool>.infiniteQueryOptions(args, { getNextPageParam })),", |
| 439 | "and write with useMutation(tools.<ns>.<tool>.mutationOptions({ onSuccess })).", |
| 440 | ].join(" "); |
| 441 | } |
| 442 | |
| 443 | const loopedHook = hookCalledInLoop(code); |
| 444 | if (loopedHook) { |
| 445 | return [ |
| 446 | `\`${loopedHook}\` is called inside a loop body.`, |
| 447 | "React hooks must run unconditionally at the top level of the component, in the same order every render.", |
| 448 | PAGINATION_HOOKS.has(loopedHook) |
| 449 | ? "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." |
| 450 | : "Move the hook out of the loop, and derive per-item values from its result instead.", |
| 451 | ].join(" "); |
| 452 | } |
| 453 | |
| 454 | const missingProvider = providerPairingRejection(code); |
| 455 | if (missingProvider) return missingProvider; |
| 456 | |
| 457 | if (REACT_DESTRUCTURING_DECLARATION.test(code)) { |
| 458 | return [ |
| 459 | "Do not destructure React in create-artifact.", |
| 460 | "Hooks such as useState are already in scope; use useState(...) directly or React.useState(...).", |
| 461 | ].join(" "); |
| 462 | } |
| 463 | |
| 464 | for (const match of code.matchAll(OBJECT_DESTRUCTURING_DECLARATION)) { |
| 465 | const names = match[1]?.split(",").flatMap((part) => { |
| 466 | const name = localDestructuredName(part); |
| 467 | return name ? [name] : []; |
| 468 | }); |
| 469 | const providedName = names?.find((name) => PROVIDED_GLOBAL_NAMES.has(name)); |
| 470 | if (providedName) { |
| 471 | return [ |
| 472 | `Provided global "${providedName}" is already in scope and cannot be redeclared.`, |
| 473 | "Remove the destructuring declaration and use the provided global directly.", |
| 474 | ].join(" "); |
| 475 | } |
| 476 | } |
| 477 | |
| 478 | for (const match of code.matchAll(PROVIDED_GLOBAL_DECLARATION)) { |
| 479 | const name = firstDefined(match[1], match[2], match[3]); |
| 480 | if (name && PROVIDED_GLOBAL_NAMES.has(name)) { |
| 481 | return [ |
| 482 | `Provided global "${name}" is already in scope and cannot be redeclared.`, |
| 483 | "Remove the local declaration and use the provided global directly.", |
| 484 | ].join(" "); |
no test coverage detected