@ggui-ai/ui-gen 0.17.0 → 0.18.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/dist/adapters/generation-dispatch.js +252 -108
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.js +261 -113
- package/dist/advanced/index.js.map +1 -1
- package/dist/boilerplate.js +27 -3
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.js +70 -0
- package/dist/check/index.js.map +1 -1
- package/dist/evaluation/axis-checks/index.js +50 -0
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.js +50 -0
- package/dist/evaluation/axis-checks/registry.js.map +1 -1
- package/dist/evaluation/index.js +50 -0
- package/dist/evaluation/index.js.map +1 -1
- package/dist/harness/index.js +27 -3
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.js +4 -0
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/runtime.js +27 -3
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.js +27 -3
- package/dist/harness/types-public.js.map +1 -1
- package/dist/index.js +261 -113
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/boilerplate/free-design-prompt.ts +2 -0
- package/src/boilerplate/generate.ts +8 -1
- package/src/boilerplate/hard-sections.ts +31 -2
- package/src/boilerplate/system-prompt.ts +2 -0
- package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +5 -3
- package/src/boilerplate/templates/layouts/fullscreen-mobile.tsx.tmpl +3 -3
- package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +5 -3
- package/src/check/run-tier0.ts +152 -0
- package/src/contract-context.ts +13 -0
- package/src/evaluation/axis-checks/checks/universal.ts +64 -0
package/dist/advanced/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { getCssTokens, fillFitRule, FORBIDDEN_IMPORT_SPECIFIERS } from '@ggui-ai
|
|
|
6
6
|
import { classifyUi, MODEL_REGISTRY, DANGEROUS_PATTERNS as DANGEROUS_PATTERNS$1, listContractGadgets, HOOK_NAME_RE, modelRefOfRoute, isGeneratorId, APP_GENERATION_PROFILE_BOUNDS, STDLIB_GADGETS_PACKAGE, resolveAppGadgets } from '@ggui-ai/protocol';
|
|
7
7
|
import fs, { existsSync, mkdirSync, writeFileSync, unlinkSync, readFileSync, rm, createReadStream, createWriteStream, readdirSync, statSync } from 'fs';
|
|
8
8
|
import { contentHash } from '@ggui-ai/protocol/content-hash';
|
|
9
|
-
import
|
|
9
|
+
import ts4 from 'typescript';
|
|
10
10
|
import path, { dirname, resolve, join, basename } from 'path';
|
|
11
11
|
import { createRequire } from 'module';
|
|
12
12
|
import { Linter } from 'eslint';
|
|
@@ -1935,53 +1935,53 @@ var init_validation = __esm({
|
|
|
1935
1935
|
}
|
|
1936
1936
|
});
|
|
1937
1937
|
function extractWireCallSites(code) {
|
|
1938
|
-
const sf =
|
|
1938
|
+
const sf = ts4.createSourceFile(
|
|
1939
1939
|
"component.tsx",
|
|
1940
1940
|
code,
|
|
1941
|
-
|
|
1941
|
+
ts4.ScriptTarget.Latest,
|
|
1942
1942
|
/*setParentNodes*/
|
|
1943
1943
|
true,
|
|
1944
|
-
|
|
1944
|
+
ts4.ScriptKind.TSX
|
|
1945
1945
|
);
|
|
1946
1946
|
const sites = [];
|
|
1947
1947
|
function visit(node) {
|
|
1948
|
-
if (
|
|
1948
|
+
if (ts4.isCallExpression(node)) {
|
|
1949
1949
|
const callee = node.expression;
|
|
1950
|
-
if (
|
|
1950
|
+
if (ts4.isIdentifier(callee)) {
|
|
1951
1951
|
const kind = HOOK_TO_KIND[callee.text];
|
|
1952
1952
|
if (kind) {
|
|
1953
1953
|
const firstArg = node.arguments[0];
|
|
1954
|
-
if (firstArg &&
|
|
1954
|
+
if (firstArg && ts4.isStringLiteral(firstArg)) {
|
|
1955
1955
|
sites.push({ kind, name: firstArg.text });
|
|
1956
1956
|
}
|
|
1957
1957
|
}
|
|
1958
1958
|
}
|
|
1959
1959
|
}
|
|
1960
|
-
|
|
1960
|
+
ts4.forEachChild(node, visit);
|
|
1961
1961
|
}
|
|
1962
1962
|
visit(sf);
|
|
1963
1963
|
return sites;
|
|
1964
1964
|
}
|
|
1965
1965
|
function extractWireImports(code) {
|
|
1966
|
-
const sf =
|
|
1966
|
+
const sf = ts4.createSourceFile(
|
|
1967
1967
|
"component.tsx",
|
|
1968
1968
|
code,
|
|
1969
|
-
|
|
1969
|
+
ts4.ScriptTarget.Latest,
|
|
1970
1970
|
/*setParentNodes*/
|
|
1971
1971
|
true,
|
|
1972
|
-
|
|
1972
|
+
ts4.ScriptKind.TSX
|
|
1973
1973
|
);
|
|
1974
1974
|
const imported = /* @__PURE__ */ new Set();
|
|
1975
1975
|
for (const stmt of sf.statements) {
|
|
1976
|
-
if (!
|
|
1976
|
+
if (!ts4.isImportDeclaration(stmt)) continue;
|
|
1977
1977
|
const moduleSpecifier = stmt.moduleSpecifier;
|
|
1978
|
-
if (!
|
|
1978
|
+
if (!ts4.isStringLiteral(moduleSpecifier)) continue;
|
|
1979
1979
|
if (moduleSpecifier.text !== "@ggui-ai/wire") continue;
|
|
1980
1980
|
const clause = stmt.importClause;
|
|
1981
1981
|
if (!clause) continue;
|
|
1982
1982
|
const bindings = clause.namedBindings;
|
|
1983
1983
|
if (!bindings) continue;
|
|
1984
|
-
if (!
|
|
1984
|
+
if (!ts4.isNamedImports(bindings)) continue;
|
|
1985
1985
|
for (const el of bindings.elements) {
|
|
1986
1986
|
imported.add(el.name.text);
|
|
1987
1987
|
}
|
|
@@ -2133,11 +2133,11 @@ var init_json_schema_ts = __esm({
|
|
|
2133
2133
|
}
|
|
2134
2134
|
});
|
|
2135
2135
|
function extractPropsInterface(code) {
|
|
2136
|
-
const sf =
|
|
2136
|
+
const sf = ts4.createSourceFile("component.tsx", code, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
|
|
2137
2137
|
const props = [];
|
|
2138
2138
|
function visitMembers(members) {
|
|
2139
2139
|
for (const member of members) {
|
|
2140
|
-
if (
|
|
2140
|
+
if (ts4.isPropertySignature(member) && member.name) {
|
|
2141
2141
|
const name = member.name.getText(sf);
|
|
2142
2142
|
const type = member.type ? member.type.getText(sf) : "unknown";
|
|
2143
2143
|
const optional = !!member.questionToken;
|
|
@@ -2146,17 +2146,17 @@ function extractPropsInterface(code) {
|
|
|
2146
2146
|
}
|
|
2147
2147
|
}
|
|
2148
2148
|
function visit(node) {
|
|
2149
|
-
if (
|
|
2149
|
+
if (ts4.isInterfaceDeclaration(node) && node.name.text === "Props") {
|
|
2150
2150
|
visitMembers(node.members);
|
|
2151
2151
|
return;
|
|
2152
2152
|
}
|
|
2153
|
-
if (
|
|
2154
|
-
if (
|
|
2153
|
+
if (ts4.isTypeAliasDeclaration(node) && node.name.text === "Props") {
|
|
2154
|
+
if (ts4.isTypeLiteralNode(node.type)) {
|
|
2155
2155
|
visitMembers(node.type.members);
|
|
2156
2156
|
}
|
|
2157
2157
|
return;
|
|
2158
2158
|
}
|
|
2159
|
-
|
|
2159
|
+
ts4.forEachChild(node, visit);
|
|
2160
2160
|
}
|
|
2161
2161
|
visit(sf);
|
|
2162
2162
|
return props.length > 0 ? props : null;
|
|
@@ -2369,12 +2369,12 @@ function generateFix(code, message, sourceLine) {
|
|
|
2369
2369
|
}
|
|
2370
2370
|
}
|
|
2371
2371
|
function parseAndStore(vfs, virtualPath, content) {
|
|
2372
|
-
const sourceFile =
|
|
2372
|
+
const sourceFile = ts4.createSourceFile(
|
|
2373
2373
|
virtualPath,
|
|
2374
2374
|
content,
|
|
2375
|
-
|
|
2375
|
+
ts4.ScriptTarget.ES2020,
|
|
2376
2376
|
true,
|
|
2377
|
-
virtualPath.endsWith(".tsx") ?
|
|
2377
|
+
virtualPath.endsWith(".tsx") ? ts4.ScriptKind.TSX : ts4.ScriptKind.TS
|
|
2378
2378
|
);
|
|
2379
2379
|
vfs.set(virtualPath, { content, sourceFile });
|
|
2380
2380
|
}
|
|
@@ -2516,12 +2516,12 @@ function resolveModuleName(vfs, name, containingFile) {
|
|
|
2516
2516
|
return { resolvedModule: void 0 };
|
|
2517
2517
|
}
|
|
2518
2518
|
function createVfsHost(vfs, componentCode) {
|
|
2519
|
-
const componentFile =
|
|
2519
|
+
const componentFile = ts4.createSourceFile(
|
|
2520
2520
|
"Component.tsx",
|
|
2521
2521
|
componentCode,
|
|
2522
|
-
|
|
2522
|
+
ts4.ScriptTarget.ES2020,
|
|
2523
2523
|
true,
|
|
2524
|
-
|
|
2524
|
+
ts4.ScriptKind.TSX
|
|
2525
2525
|
);
|
|
2526
2526
|
const host = {
|
|
2527
2527
|
getSourceFile(fileName) {
|
|
@@ -2576,7 +2576,7 @@ function resolved(resolvedFileName) {
|
|
|
2576
2576
|
resolvedModule: {
|
|
2577
2577
|
resolvedFileName,
|
|
2578
2578
|
isExternalLibraryImport: true,
|
|
2579
|
-
extension:
|
|
2579
|
+
extension: ts4.Extension.Dts
|
|
2580
2580
|
}
|
|
2581
2581
|
};
|
|
2582
2582
|
}
|
|
@@ -2598,18 +2598,18 @@ async function typecheck(code, dtsMap) {
|
|
|
2598
2598
|
const prefixedCode = SYNTHETIC_PREFIX + code;
|
|
2599
2599
|
const lineOffset = SYNTHETIC_PREFIX.split("\n").length - 1;
|
|
2600
2600
|
const host = createVfsHost(effectiveVfs, prefixedCode);
|
|
2601
|
-
const program =
|
|
2602
|
-
const diagnostics =
|
|
2601
|
+
const program = ts4.createProgram(["Component.tsx"], COMPILER_OPTIONS, host);
|
|
2602
|
+
const diagnostics = ts4.getPreEmitDiagnostics(program);
|
|
2603
2603
|
const errors = [];
|
|
2604
2604
|
const warnings = [];
|
|
2605
2605
|
for (const diag of diagnostics) {
|
|
2606
2606
|
if (diag.file && diag.file.fileName !== "Component.tsx") continue;
|
|
2607
2607
|
if (!diag.file) continue;
|
|
2608
|
-
const diagLine =
|
|
2608
|
+
const diagLine = ts4.getLineAndCharacterOfPosition(diag.file, diag.start ?? 0).line;
|
|
2609
2609
|
if (diagLine < lineOffset) continue;
|
|
2610
|
-
const rawLine = diag.file ?
|
|
2610
|
+
const rawLine = diag.file ? ts4.getLineAndCharacterOfPosition(diag.file, diag.start ?? 0).line + 1 : 0;
|
|
2611
2611
|
const line = Math.max(1, rawLine - lineOffset);
|
|
2612
|
-
const message =
|
|
2612
|
+
const message = ts4.flattenDiagnosticMessageText(diag.messageText, "\n");
|
|
2613
2613
|
const code_ = diag.code;
|
|
2614
2614
|
const sourceLines = diag.file.text.split("\n");
|
|
2615
2615
|
const sourceLine = rawLine >= 1 && rawLine <= sourceLines.length ? sourceLines[rawLine - 1]?.trim() ?? "" : "";
|
|
@@ -2656,8 +2656,8 @@ var init_type_checker = __esm({
|
|
|
2656
2656
|
]);
|
|
2657
2657
|
vfsCache = null;
|
|
2658
2658
|
COMPILER_OPTIONS = {
|
|
2659
|
-
target:
|
|
2660
|
-
module:
|
|
2659
|
+
target: ts4.ScriptTarget.ES2020,
|
|
2660
|
+
module: ts4.ModuleKind.ESNext,
|
|
2661
2661
|
// Classic React JSX mode. The synthetic prefix (SYNTHETIC_PREFIX)
|
|
2662
2662
|
// supplies `import React from 'react'` plus a `declare global` JSX
|
|
2663
2663
|
// namespace shim: `@types/react` v19 removed the *global* `JSX`
|
|
@@ -2665,8 +2665,8 @@ var init_type_checker = __esm({
|
|
|
2665
2665
|
// for `JSX.IntrinsicElements` and the `key`/`ref` carve-out. Without
|
|
2666
2666
|
// the shim, `<div>` degrades to `any` and every typed component
|
|
2667
2667
|
// falsely rejects the intrinsic `key` prop.
|
|
2668
|
-
jsx:
|
|
2669
|
-
moduleResolution:
|
|
2668
|
+
jsx: ts4.JsxEmit.React,
|
|
2669
|
+
moduleResolution: ts4.ModuleResolutionKind.Bundler,
|
|
2670
2670
|
strict: false,
|
|
2671
2671
|
strictNullChecks: true,
|
|
2672
2672
|
// Catch undefined.foo() errors that cause runtime crashes
|
|
@@ -3158,17 +3158,17 @@ Only list ACTUAL composition problems. Use 'fail' only for a fundamentally incoh
|
|
|
3158
3158
|
}
|
|
3159
3159
|
});
|
|
3160
3160
|
function stripComments(code) {
|
|
3161
|
-
const scanner =
|
|
3162
|
-
|
|
3161
|
+
const scanner = ts4.createScanner(
|
|
3162
|
+
ts4.ScriptTarget.Latest,
|
|
3163
3163
|
/* skipTrivia */
|
|
3164
3164
|
false,
|
|
3165
|
-
|
|
3165
|
+
ts4.LanguageVariant.JSX,
|
|
3166
3166
|
code
|
|
3167
3167
|
);
|
|
3168
3168
|
let out = "";
|
|
3169
|
-
for (let token = scanner.scan(); token !==
|
|
3169
|
+
for (let token = scanner.scan(); token !== ts4.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
|
|
3170
3170
|
const text = scanner.getTokenText();
|
|
3171
|
-
out += token ===
|
|
3171
|
+
out += token === ts4.SyntaxKind.SingleLineCommentTrivia || token === ts4.SyntaxKind.MultiLineCommentTrivia ? text.replace(/[^\n]/g, " ") : text;
|
|
3172
3172
|
}
|
|
3173
3173
|
return out;
|
|
3174
3174
|
}
|
|
@@ -3191,20 +3191,20 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode
|
|
|
3191
3191
|
"Link"
|
|
3192
3192
|
]);
|
|
3193
3193
|
const HANDLER_ATTRS = ["onClick", "onChange", "onPress", "onSelect"];
|
|
3194
|
-
const sf =
|
|
3194
|
+
const sf = ts4.createSourceFile(
|
|
3195
3195
|
"source.tsx",
|
|
3196
3196
|
sourceCode,
|
|
3197
|
-
|
|
3197
|
+
ts4.ScriptTarget.Latest,
|
|
3198
3198
|
/* setParentNodes */
|
|
3199
3199
|
true,
|
|
3200
|
-
|
|
3200
|
+
ts4.ScriptKind.TSX
|
|
3201
3201
|
);
|
|
3202
3202
|
function tagNameText(node) {
|
|
3203
|
-
return
|
|
3203
|
+
return ts4.isIdentifier(node.tagName) ? node.tagName.text : void 0;
|
|
3204
3204
|
}
|
|
3205
3205
|
function attrExpression(node, attrName) {
|
|
3206
3206
|
for (const attr of node.attributes.properties) {
|
|
3207
|
-
if (
|
|
3207
|
+
if (ts4.isJsxAttribute(attr) && ts4.isIdentifier(attr.name) && attr.name.text === attrName && attr.initializer !== void 0 && ts4.isJsxExpression(attr.initializer)) {
|
|
3208
3208
|
return attr.initializer.expression;
|
|
3209
3209
|
}
|
|
3210
3210
|
}
|
|
@@ -3216,19 +3216,19 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode
|
|
|
3216
3216
|
if (anyElementIsInteractive) return true;
|
|
3217
3217
|
if (!TRAIT_HOST_TAGS.has(tag)) return false;
|
|
3218
3218
|
const asExpr = attrExpression(node, "as");
|
|
3219
|
-
return asExpr !== void 0 &&
|
|
3219
|
+
return asExpr !== void 0 && ts4.isIdentifier(asExpr) && TRAITS_THAT_FIRE.has(asExpr.text);
|
|
3220
3220
|
}
|
|
3221
3221
|
function extractCalleeName(expr) {
|
|
3222
3222
|
if (expr === void 0) return void 0;
|
|
3223
|
-
if (
|
|
3224
|
-
if (
|
|
3223
|
+
if (ts4.isIdentifier(expr)) return expr.text;
|
|
3224
|
+
if (ts4.isArrowFunction(expr) || ts4.isFunctionExpression(expr)) {
|
|
3225
3225
|
const body = expr.body;
|
|
3226
|
-
if (
|
|
3226
|
+
if (ts4.isCallExpression(body) && ts4.isIdentifier(body.expression)) {
|
|
3227
3227
|
return body.expression.text;
|
|
3228
3228
|
}
|
|
3229
|
-
if (
|
|
3229
|
+
if (ts4.isBlock(body)) {
|
|
3230
3230
|
for (const stmt of body.statements) {
|
|
3231
|
-
if (
|
|
3231
|
+
if (ts4.isExpressionStatement(stmt) && ts4.isCallExpression(stmt.expression) && ts4.isIdentifier(stmt.expression.expression)) {
|
|
3232
3232
|
return stmt.expression.expression.text;
|
|
3233
3233
|
}
|
|
3234
3234
|
}
|
|
@@ -3240,7 +3240,7 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode
|
|
|
3240
3240
|
let found;
|
|
3241
3241
|
function walk(node) {
|
|
3242
3242
|
if (found !== void 0) return;
|
|
3243
|
-
const opening =
|
|
3243
|
+
const opening = ts4.isJsxElement(node) ? node.openingElement : ts4.isJsxSelfClosingElement(node) ? node : void 0;
|
|
3244
3244
|
if (opening !== void 0) {
|
|
3245
3245
|
const tag = tagNameText(opening);
|
|
3246
3246
|
if (tag !== void 0 && (anyElementIsInteractive || INTERACTIVE_DESCENDANTS.has(tag))) {
|
|
@@ -3256,13 +3256,13 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode
|
|
|
3256
3256
|
}
|
|
3257
3257
|
}
|
|
3258
3258
|
}
|
|
3259
|
-
|
|
3259
|
+
ts4.forEachChild(node, walk);
|
|
3260
3260
|
}
|
|
3261
|
-
|
|
3261
|
+
ts4.forEachChild(outer, walk);
|
|
3262
3262
|
return found;
|
|
3263
3263
|
}
|
|
3264
3264
|
function visit(node) {
|
|
3265
|
-
if (
|
|
3265
|
+
if (ts4.isJsxElement(node) && isTraitHostWithFiringAs(node.openingElement)) {
|
|
3266
3266
|
const outerCallee = extractCalleeName(attrExpression(node.openingElement, "onClick")) ?? extractCalleeName(attrExpression(node.openingElement, "onPress"));
|
|
3267
3267
|
if (outerCallee !== void 0 && bindings.has(outerCallee)) {
|
|
3268
3268
|
const match = findMatchingInteractiveDescendant(node, outerCallee);
|
|
@@ -3283,7 +3283,7 @@ function detectCertainDoubleWiredActions(sourceCode, actionBindings, designMode
|
|
|
3283
3283
|
}
|
|
3284
3284
|
}
|
|
3285
3285
|
}
|
|
3286
|
-
|
|
3286
|
+
ts4.forEachChild(node, visit);
|
|
3287
3287
|
}
|
|
3288
3288
|
visit(sf);
|
|
3289
3289
|
return issues;
|
|
@@ -3302,6 +3302,63 @@ function isGadgetExportImported(source, pkg, exportName) {
|
|
|
3302
3302
|
}
|
|
3303
3303
|
return false;
|
|
3304
3304
|
}
|
|
3305
|
+
function detectComponentNeverReturns(sourceCode) {
|
|
3306
|
+
const sf = ts4.createSourceFile(
|
|
3307
|
+
"source.tsx",
|
|
3308
|
+
sourceCode,
|
|
3309
|
+
ts4.ScriptTarget.Latest,
|
|
3310
|
+
/* setParentNodes */
|
|
3311
|
+
true,
|
|
3312
|
+
ts4.ScriptKind.TSX
|
|
3313
|
+
);
|
|
3314
|
+
const isFunctionLike = (n) => ts4.isFunctionDeclaration(n) || ts4.isFunctionExpression(n) || ts4.isArrowFunction(n) || ts4.isMethodDeclaration(n) || ts4.isClassDeclaration(n) || ts4.isClassExpression(n);
|
|
3315
|
+
function resolveTopLevel(name) {
|
|
3316
|
+
for (const st of sf.statements) {
|
|
3317
|
+
if (ts4.isFunctionDeclaration(st) && st.name?.text === name) return st;
|
|
3318
|
+
if (ts4.isVariableStatement(st)) {
|
|
3319
|
+
for (const decl of st.declarationList.declarations) {
|
|
3320
|
+
if (ts4.isIdentifier(decl.name) && decl.name.text === name && decl.initializer !== void 0 && (ts4.isArrowFunction(decl.initializer) || ts4.isFunctionExpression(decl.initializer))) {
|
|
3321
|
+
return decl.initializer;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
return void 0;
|
|
3327
|
+
}
|
|
3328
|
+
let component;
|
|
3329
|
+
for (const st of sf.statements) {
|
|
3330
|
+
if (ts4.isFunctionDeclaration(st) && st.modifiers?.some((m) => m.kind === ts4.SyntaxKind.DefaultKeyword)) {
|
|
3331
|
+
component = st;
|
|
3332
|
+
break;
|
|
3333
|
+
}
|
|
3334
|
+
if (ts4.isExportAssignment(st) && st.isExportEquals !== true) {
|
|
3335
|
+
const expr = st.expression;
|
|
3336
|
+
if (ts4.isArrowFunction(expr) || ts4.isFunctionExpression(expr)) component = expr;
|
|
3337
|
+
else if (ts4.isIdentifier(expr)) component = resolveTopLevel(expr.text);
|
|
3338
|
+
break;
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
if (component === void 0) return null;
|
|
3342
|
+
const body = component.body;
|
|
3343
|
+
if (body === void 0 || !ts4.isBlock(body)) return null;
|
|
3344
|
+
let returnsValue = false;
|
|
3345
|
+
let jsxStatementLine;
|
|
3346
|
+
const visit = (node) => {
|
|
3347
|
+
if (returnsValue) return;
|
|
3348
|
+
if (ts4.isReturnStatement(node)) {
|
|
3349
|
+
if (node.expression !== void 0) returnsValue = true;
|
|
3350
|
+
return;
|
|
3351
|
+
}
|
|
3352
|
+
if (jsxStatementLine === void 0 && ts4.isExpressionStatement(node) && (ts4.isJsxElement(node.expression) || ts4.isJsxFragment(node.expression) || ts4.isJsxSelfClosingElement(node.expression))) {
|
|
3353
|
+
jsxStatementLine = sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1;
|
|
3354
|
+
}
|
|
3355
|
+
node.forEachChild((child) => {
|
|
3356
|
+
if (!isFunctionLike(child)) visit(child);
|
|
3357
|
+
});
|
|
3358
|
+
};
|
|
3359
|
+
for (const st of body.statements) if (!isFunctionLike(st)) visit(st);
|
|
3360
|
+
return returnsValue ? null : { jsxStatementLine };
|
|
3361
|
+
}
|
|
3305
3362
|
async function runTier0Checks(sourceCode, contract, buildErrors, gadgetTypes, designMode = DEFAULT_DESIGN_MODE) {
|
|
3306
3363
|
const issues = [];
|
|
3307
3364
|
const lines = sourceCode.split("\n");
|
|
@@ -3721,6 +3778,19 @@ ${errorDetail}`,
|
|
|
3721
3778
|
fix: "Add export default function Component(props: Props) { ... }"
|
|
3722
3779
|
});
|
|
3723
3780
|
}
|
|
3781
|
+
const neverReturns = detectComponentNeverReturns(sourceCode);
|
|
3782
|
+
if (neverReturns !== null) {
|
|
3783
|
+
issues.push({
|
|
3784
|
+
tier: 0,
|
|
3785
|
+
result: "fail",
|
|
3786
|
+
category: "compile",
|
|
3787
|
+
subcategory: "renders-nothing",
|
|
3788
|
+
severity: "critical",
|
|
3789
|
+
description: neverReturns.jsxStatementLine === void 0 ? "The component never returns its tree \u2014 it renders a blank frame" : `The component never returns its tree: the JSX at line ${neverReturns.jsxStatementLine} sits at statement position, so it is built and thrown away and the frame renders blank`,
|
|
3790
|
+
fix: "Wrap the tree in the return the boilerplate gave you: `return (` before the root element and `);` after it.",
|
|
3791
|
+
...neverReturns.jsxStatementLine === void 0 ? {} : { line: neverReturns.jsxStatementLine }
|
|
3792
|
+
});
|
|
3793
|
+
}
|
|
3724
3794
|
if (contract) {
|
|
3725
3795
|
try {
|
|
3726
3796
|
const contractIssues = validateAllContracts(sourceCode, contract);
|
|
@@ -11979,18 +12049,18 @@ ${context}
|
|
|
11979
12049
|
}
|
|
11980
12050
|
var LABELED_COMPONENTS = /* @__PURE__ */ new Set(["Input", "TextArea", "Select"]);
|
|
11981
12051
|
function checkReactRules(code) {
|
|
11982
|
-
const sf =
|
|
12052
|
+
const sf = ts4.createSourceFile("component.tsx", code, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
|
|
11983
12053
|
const issues = [];
|
|
11984
12054
|
function visit(node) {
|
|
11985
|
-
if (
|
|
12055
|
+
if (ts4.isJsxSelfClosingElement(node) || ts4.isJsxOpeningElement(node)) {
|
|
11986
12056
|
const tagName = node.tagName.getText(sf);
|
|
11987
12057
|
if (LABELED_COMPONENTS.has(tagName)) {
|
|
11988
12058
|
const propNames = /* @__PURE__ */ new Set();
|
|
11989
12059
|
let hasSpread = false;
|
|
11990
12060
|
for (const attr of node.attributes.properties) {
|
|
11991
|
-
if (
|
|
12061
|
+
if (ts4.isJsxAttribute(attr)) {
|
|
11992
12062
|
propNames.add(attr.name.getText(sf));
|
|
11993
|
-
} else if (
|
|
12063
|
+
} else if (ts4.isJsxSpreadAttribute(attr)) {
|
|
11994
12064
|
hasSpread = true;
|
|
11995
12065
|
}
|
|
11996
12066
|
}
|
|
@@ -12005,7 +12075,7 @@ function checkReactRules(code) {
|
|
|
12005
12075
|
}
|
|
12006
12076
|
}
|
|
12007
12077
|
}
|
|
12008
|
-
|
|
12078
|
+
ts4.forEachChild(node, visit);
|
|
12009
12079
|
}
|
|
12010
12080
|
visit(sf);
|
|
12011
12081
|
return issues;
|
|
@@ -13174,6 +13244,50 @@ function runViewportSized(input) {
|
|
|
13174
13244
|
)
|
|
13175
13245
|
];
|
|
13176
13246
|
}
|
|
13247
|
+
var TERMINAL_WORDS = /* @__PURE__ */ new Set([
|
|
13248
|
+
"submit",
|
|
13249
|
+
"confirm",
|
|
13250
|
+
"approve",
|
|
13251
|
+
"schedule",
|
|
13252
|
+
"reserve",
|
|
13253
|
+
"book",
|
|
13254
|
+
"pay",
|
|
13255
|
+
"checkout",
|
|
13256
|
+
"purchase",
|
|
13257
|
+
"order"
|
|
13258
|
+
]);
|
|
13259
|
+
var TERMINAL_LABEL_RX = /\b(submit|confirm|approve|schedule|reserve|book|pay|checkout|purchase|place\s+order)\b/i;
|
|
13260
|
+
var GUARD_RX = /\b(disabled|aria-disabled)\s*=/;
|
|
13261
|
+
function nameWords(name) {
|
|
13262
|
+
return name.split(/(?=[A-Z])|[_\-\s]+/).map((w) => w.toLowerCase()).filter((w) => w.length > 0);
|
|
13263
|
+
}
|
|
13264
|
+
function findUnguardedTerminalControls(sourceCode) {
|
|
13265
|
+
if (GUARD_RX.test(sourceCode)) return [];
|
|
13266
|
+
const names = /* @__PURE__ */ new Set();
|
|
13267
|
+
for (const m of sourceCode.matchAll(/useAction(?:<[^>]*>)?\(\s*['"`]([^'"`]+)['"`]/g)) {
|
|
13268
|
+
const name = m[1] ?? "";
|
|
13269
|
+
if (nameWords(name).some((w) => TERMINAL_WORDS.has(w))) names.add(name);
|
|
13270
|
+
}
|
|
13271
|
+
for (const m of sourceCode.matchAll(/>([^<>{}]{1,60})<\/Button>/g)) {
|
|
13272
|
+
const label = (m[1] ?? "").replace(/\s+/g, " ").trim();
|
|
13273
|
+
if (label.length > 0 && TERMINAL_LABEL_RX.test(label)) names.add(label.slice(0, 40));
|
|
13274
|
+
}
|
|
13275
|
+
return [...names];
|
|
13276
|
+
}
|
|
13277
|
+
function runTerminalActionUnguarded(input) {
|
|
13278
|
+
if (input.compiledCode === null) return [];
|
|
13279
|
+
const found = findUnguardedTerminalControls(input.sourceCode);
|
|
13280
|
+
if (found.length === 0) return [];
|
|
13281
|
+
const list = found.map((t) => `"${t}"`).join(", ");
|
|
13282
|
+
return [
|
|
13283
|
+
mkIssue(
|
|
13284
|
+
"universal.terminal_action_unguarded",
|
|
13285
|
+
`${list} reads as an action the user means once, and nothing in this component disables the control after it fires \u2014 a double-click sends it twice.`,
|
|
13286
|
+
'Hold the fired state (`const [submitted, setSubmitted] = useState(false)`), set it in the handler, and pass `disabled={submitted}` to that control with a word that says so ("Submitted"). If the action IS meant to repeat \u2014 sending a message, adding an item \u2014 leave it armed and ignore this.',
|
|
13287
|
+
"warn"
|
|
13288
|
+
)
|
|
13289
|
+
];
|
|
13290
|
+
}
|
|
13177
13291
|
var UNIVERSAL_CHECKS = [
|
|
13178
13292
|
{
|
|
13179
13293
|
id: "universal.icon_name_known",
|
|
@@ -13193,6 +13307,12 @@ var UNIVERSAL_CHECKS = [
|
|
|
13193
13307
|
values: ALL_RENDER_VALUES,
|
|
13194
13308
|
run: runViewportSized
|
|
13195
13309
|
},
|
|
13310
|
+
{
|
|
13311
|
+
id: "universal.terminal_action_unguarded",
|
|
13312
|
+
axis: "render",
|
|
13313
|
+
values: ALL_RENDER_VALUES,
|
|
13314
|
+
run: runTerminalActionUnguarded
|
|
13315
|
+
},
|
|
13196
13316
|
{
|
|
13197
13317
|
id: "universal.accent_text_ink",
|
|
13198
13318
|
axis: "render",
|
|
@@ -15058,7 +15178,8 @@ type ${typeName} = ${tsType};`
|
|
|
15058
15178
|
);
|
|
15059
15179
|
const callSig = tsType === "void" ? "() => void \u2014 fire and forget" : `(data: ${tsType}) => void`;
|
|
15060
15180
|
const toolHint = tool ? ` \u2192 nextStep: ${tool}` : "";
|
|
15061
|
-
|
|
15181
|
+
const onceHint = " \u2014 if this is meant ONCE, disable its control after it fires";
|
|
15182
|
+
actionHookCalls.push(` const ${key} = useAction<${typeName}>('${key}'); // ${callSig}${toolHint}${onceHint}`);
|
|
15062
15183
|
}
|
|
15063
15184
|
const streamChannels = contract?.streamSpec ?? {};
|
|
15064
15185
|
const streamChannelEntries = Object.entries(streamChannels);
|
|
@@ -15404,12 +15525,33 @@ Rules:
|
|
|
15404
15525
|
3. Default values come from the current request \u2014 so the component renders correctly standalone.
|
|
15405
15526
|
4. Copy comes from the props. NEVER invent display text the contract does not carry \u2014 no eyebrow / kicker / overline (a \`<Text caps>\` label), no helper line, no status text, no tagline. A caps label exists only when a prop supplies its text (\`<Text caps>{props.section}</Text>\`); a heading's fallback is the request's own words, never a slogan of your own. Text the agent cannot change is chatter on every render.
|
|
15406
15527
|
5. Props interface must be typed and exported.`;
|
|
15407
|
-
var
|
|
15528
|
+
var TERMINAL_ACTIONS = `## Actions the user means once
|
|
15529
|
+
|
|
15530
|
+
A second press is a mistake only when it would make a SECOND THING HAPPEN IN THE WORLD \u2014 a second booking, a second charge, a second order, a second application sent. If pressing again would only write the SAME STATE AGAIN \u2014 saving, updating, renaming, setting a preference, toggling \u2014 nothing is duplicated and the control stays armed.
|
|
15531
|
+
|
|
15532
|
+
Guard a control only where you have evidence, not a feeling about the verb: the request itself names a one-time commitment \u2014 "submit the application", "place the order", "confirm the booking".
|
|
15533
|
+
|
|
15534
|
+
**When in doubt, leave it armed.** A control the user cannot press a second time has lost them something that worked \u2014 save, edit, save again is ordinary. A control left armed is only what every interface does by default.
|
|
15535
|
+
|
|
15536
|
+
When you do guard one:
|
|
15537
|
+
- Hold a small piece of state for it and set it in the handler: \`const [submitted, setSubmitted] = useState(false)\`.
|
|
15538
|
+
- \`disabled={submitted}\` on the control that fires it, and say so in the label or beside it ("Submitted", "Request sent") \u2014 a control that is disabled without explanation reads as broken.
|
|
15539
|
+
- A card re-rendered with new props starts fresh; the state above lives with the component, not with the request.
|
|
15540
|
+
|
|
15541
|
+
Nothing in the wire DECLARES one-shot-ness yet, which is why the test is what a second press would DO \u2014 not how final the word sounds.`;
|
|
15542
|
+
var FRAME_SIZING = `## Sizing \u2014 the frame owns the height and the width
|
|
15408
15543
|
|
|
15409
15544
|
The host sizes the iframe; the component never sizes itself to the viewport. NEVER write \`100vh\` / \`100dvh\` / \`100svh\` (or an \`h-screen\` / \`min-h-screen\` class) as a height on any element:
|
|
15410
15545
|
- An inline card takes its natural height. A content-sized frame is measured FROM the content, so a \`100vh\` root can never shrink and grows the frame on every re-measure.
|
|
15411
15546
|
- Under fullscreen the frame already stretches your root to its full height. Centre inside it with flex (\`display: flex; flex-direction: column; justify-content: center\`), never with a viewport height.
|
|
15412
|
-
- To fill a parent that has a height use \`height: 100%\` or \`flex: 1\`; a viewport unit is only ever a \`max-height\` cap on a scroll region
|
|
15547
|
+
- To fill a parent that has a height use \`height: 100%\` or \`flex: 1\`; a viewport unit is only ever a \`max-height\` cap on a scroll region.
|
|
15548
|
+
|
|
15549
|
+
The width works the same way, and the mistake is the mirror image: **your outermost element FILLS the frame; a width cap belongs INSIDE it, on the column that holds text.**
|
|
15550
|
+
|
|
15551
|
+
- Never put \`maxWidth\` (or a \`max-w-*\` class) on the element you return at the top. On a wide canvas that turns your card into a narrow strip with the frame's ground on either side of it, and on a narrow one it does nothing \u2014 the same composition reading differently at two sizes.
|
|
15552
|
+
- DO cap the reading measure inside: a column of prose is easiest to read at 60\u201370 characters, so wrap the TEXT in a \`<Container maxWidth="sm">\` (or your own \`max-width\`) and leave the root filling. A hero band, a toolbar, a table or an image then still uses the whole width, which is what makes a wide card look composed rather than padded.
|
|
15553
|
+
- Centre that inner column with the container's own centring, not by shrinking the root.
|
|
15554
|
+
- **They are two different elements.** Even when the whole card is one column of text, the root still fills and the column sits INSIDE it \u2014 \`<Box padding="lg"><Container maxWidth="sm">\u2026</Container></Box>\`, never a root that is itself the container. A centred card is centring inside a filled frame, not a shrunken frame.`;
|
|
15413
15555
|
var COMPONENT_STRUCTURE = `## Component Structure
|
|
15414
15556
|
|
|
15415
15557
|
Keep JSX nesting depth to 3\u20135 levels. When deeper, extract repeated/complex sections into helper components \u2014 named functions defined above the main Component in the same file. Helpers take data + callbacks via props; they don't own state.
|
|
@@ -15621,6 +15763,7 @@ ${axisSection}
|
|
|
15621
15763
|
CONTRACT_SURFACE,
|
|
15622
15764
|
DEFENSIVE_CODING,
|
|
15623
15765
|
GESTURE_ROUTING,
|
|
15766
|
+
TERMINAL_ACTIONS,
|
|
15624
15767
|
FREE_GESTURE_SURFACE,
|
|
15625
15768
|
ANTI_PATTERNS,
|
|
15626
15769
|
CROSS_REFERENCE_RULES,
|
|
@@ -15651,11 +15794,11 @@ ${inputs.primitivesDoc}`
|
|
|
15651
15794
|
}
|
|
15652
15795
|
var VIRTUAL_DTS_PATH = "/__gadget__.d.ts";
|
|
15653
15796
|
var EXTRACTOR_COMPILER_OPTIONS = {
|
|
15654
|
-
target:
|
|
15655
|
-
module:
|
|
15656
|
-
moduleResolution:
|
|
15797
|
+
target: ts4.ScriptTarget.ESNext,
|
|
15798
|
+
module: ts4.ModuleKind.ESNext,
|
|
15799
|
+
moduleResolution: ts4.ModuleResolutionKind.Bundler,
|
|
15657
15800
|
allowJs: false,
|
|
15658
|
-
jsx:
|
|
15801
|
+
jsx: ts4.JsxEmit.ReactJSX,
|
|
15659
15802
|
noEmit: true,
|
|
15660
15803
|
esModuleInterop: true,
|
|
15661
15804
|
// skipLibCheck so an unresolvable wrapper-internal import (the
|
|
@@ -15667,31 +15810,31 @@ var EXTRACTOR_COMPILER_OPTIONS = {
|
|
|
15667
15810
|
var callSignatureCache = /* @__PURE__ */ new Map();
|
|
15668
15811
|
var componentPropsCache = /* @__PURE__ */ new Map();
|
|
15669
15812
|
function buildExtractorContext(dtsContent, names) {
|
|
15670
|
-
const sourceFile =
|
|
15813
|
+
const sourceFile = ts4.createSourceFile(
|
|
15671
15814
|
VIRTUAL_DTS_PATH,
|
|
15672
15815
|
dtsContent,
|
|
15673
|
-
|
|
15816
|
+
ts4.ScriptTarget.ESNext,
|
|
15674
15817
|
/* setParentNodes */
|
|
15675
15818
|
true,
|
|
15676
|
-
|
|
15819
|
+
ts4.ScriptKind.TS
|
|
15677
15820
|
);
|
|
15678
|
-
const defaultLibName =
|
|
15821
|
+
const defaultLibName = ts4.getDefaultLibFileName(EXTRACTOR_COMPILER_OPTIONS);
|
|
15679
15822
|
const host = {
|
|
15680
15823
|
getSourceFile(fileName) {
|
|
15681
15824
|
if (fileName === VIRTUAL_DTS_PATH) return sourceFile;
|
|
15682
|
-
const libContent =
|
|
15683
|
-
|
|
15825
|
+
const libContent = ts4.sys.readFile(
|
|
15826
|
+
ts4.getDefaultLibFilePath(EXTRACTOR_COMPILER_OPTIONS).replace(
|
|
15684
15827
|
/[^/\\]+$/,
|
|
15685
15828
|
fileName
|
|
15686
15829
|
)
|
|
15687
15830
|
);
|
|
15688
15831
|
if (libContent !== void 0) {
|
|
15689
|
-
return
|
|
15832
|
+
return ts4.createSourceFile(
|
|
15690
15833
|
fileName,
|
|
15691
15834
|
libContent,
|
|
15692
|
-
|
|
15835
|
+
ts4.ScriptTarget.ESNext,
|
|
15693
15836
|
true,
|
|
15694
|
-
|
|
15837
|
+
ts4.ScriptKind.TS
|
|
15695
15838
|
);
|
|
15696
15839
|
}
|
|
15697
15840
|
return void 0;
|
|
@@ -15715,14 +15858,14 @@ function buildExtractorContext(dtsContent, names) {
|
|
|
15715
15858
|
},
|
|
15716
15859
|
fileExists(f) {
|
|
15717
15860
|
if (f === VIRTUAL_DTS_PATH) return true;
|
|
15718
|
-
return
|
|
15861
|
+
return ts4.sys.fileExists(f);
|
|
15719
15862
|
},
|
|
15720
15863
|
readFile(f) {
|
|
15721
15864
|
if (f === VIRTUAL_DTS_PATH) return dtsContent;
|
|
15722
|
-
return
|
|
15865
|
+
return ts4.sys.readFile(f);
|
|
15723
15866
|
}
|
|
15724
15867
|
};
|
|
15725
|
-
const program =
|
|
15868
|
+
const program = ts4.createProgram(
|
|
15726
15869
|
[VIRTUAL_DTS_PATH],
|
|
15727
15870
|
EXTRACTOR_COMPILER_OPTIONS,
|
|
15728
15871
|
host
|
|
@@ -15740,9 +15883,9 @@ function buildExtractorContext(dtsContent, names) {
|
|
|
15740
15883
|
}
|
|
15741
15884
|
const MAX_DEPTH2 = 4;
|
|
15742
15885
|
function renderType(t, depth) {
|
|
15743
|
-
const plain = () => checker.typeToString(t, void 0,
|
|
15886
|
+
const plain = () => checker.typeToString(t, void 0, ts4.TypeFormatFlags.NoTruncation);
|
|
15744
15887
|
if (depth >= MAX_DEPTH2) return plain();
|
|
15745
|
-
if ((t.getFlags() &
|
|
15888
|
+
if ((t.getFlags() & ts4.TypeFlags.Boolean) !== 0) return "boolean";
|
|
15746
15889
|
if (t.isUnion()) {
|
|
15747
15890
|
return t.types.map((member) => renderType(member, depth)).join(" | ");
|
|
15748
15891
|
}
|
|
@@ -15756,13 +15899,13 @@ function buildExtractorContext(dtsContent, names) {
|
|
|
15756
15899
|
const params = cs.getParameters().map((p) => {
|
|
15757
15900
|
const decl = p.valueDeclaration ?? p.declarations?.[0];
|
|
15758
15901
|
const pType = decl !== void 0 ? checker.getTypeOfSymbolAtLocation(p, decl) : checker.getDeclaredTypeOfSymbol(p);
|
|
15759
|
-
const optional = decl !== void 0 &&
|
|
15902
|
+
const optional = decl !== void 0 && ts4.isParameter(decl) && (decl.questionToken !== void 0 || decl.initializer !== void 0);
|
|
15760
15903
|
return `${p.getName()}${optional ? "?" : ""}: ${renderType(pType, depth + 1)}`;
|
|
15761
15904
|
}).join(", ");
|
|
15762
15905
|
const ret = renderType(cs.getReturnType(), depth + 1);
|
|
15763
15906
|
return `(${params}) => ${ret}`;
|
|
15764
15907
|
}
|
|
15765
|
-
const isObject = (t.getFlags() &
|
|
15908
|
+
const isObject = (t.getFlags() & ts4.TypeFlags.Object) !== 0;
|
|
15766
15909
|
const isArrayOrTuple = checker.isArrayType(t) || checker.isTupleType(t);
|
|
15767
15910
|
const typeArgs = t.typeArguments;
|
|
15768
15911
|
if (symbol !== void 0 && isObject && !isArrayOrTuple && !isWrapperLocal(symbol) && typeArgs !== void 0 && typeArgs.length > 0) {
|
|
@@ -15775,7 +15918,7 @@ function buildExtractorContext(dtsContent, names) {
|
|
|
15775
15918
|
const body = props.map((p) => {
|
|
15776
15919
|
const decl = p.valueDeclaration ?? p.declarations?.[0];
|
|
15777
15920
|
const pType = decl !== void 0 ? checker.getTypeOfSymbolAtLocation(p, decl) : checker.getDeclaredTypeOfSymbol(p);
|
|
15778
|
-
const optional = (p.getFlags() &
|
|
15921
|
+
const optional = (p.getFlags() & ts4.SymbolFlags.Optional) !== 0;
|
|
15779
15922
|
return `${p.getName()}${optional ? "?" : ""}: ${renderType(pType, depth + 1)}`;
|
|
15780
15923
|
}).join("; ");
|
|
15781
15924
|
return `{ ${body} }`;
|
|
@@ -15790,7 +15933,7 @@ function buildExtractorContext(dtsContent, names) {
|
|
|
15790
15933
|
const name = symbol.getName();
|
|
15791
15934
|
if (!requested.has(name)) continue;
|
|
15792
15935
|
if (signaturesByName.has(name)) continue;
|
|
15793
|
-
const resolved2 = (symbol.flags &
|
|
15936
|
+
const resolved2 = (symbol.flags & ts4.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(symbol) : symbol;
|
|
15794
15937
|
const declarations = resolved2.getDeclarations();
|
|
15795
15938
|
if (declarations === void 0 || declarations.length === 0) continue;
|
|
15796
15939
|
const declaration = declarations[0];
|
|
@@ -15805,7 +15948,7 @@ function printCallSignature(sig, ctx) {
|
|
|
15805
15948
|
const params = sig.getParameters().map((p) => {
|
|
15806
15949
|
const decl = p.valueDeclaration ?? p.declarations?.[0];
|
|
15807
15950
|
const pType = decl !== void 0 ? ctx.checker.getTypeOfSymbolAtLocation(p, decl) : ctx.checker.getDeclaredTypeOfSymbol(p);
|
|
15808
|
-
const optional = decl !== void 0 &&
|
|
15951
|
+
const optional = decl !== void 0 && ts4.isParameter(decl) && (decl.questionToken !== void 0 || decl.initializer !== void 0);
|
|
15809
15952
|
return `${p.getName()}${optional ? "?" : ""}: ${ctx.renderType(pType, 0)}`;
|
|
15810
15953
|
}).join(", ");
|
|
15811
15954
|
const ret = ctx.renderType(sig.getReturnType(), 0);
|
|
@@ -16019,6 +16162,7 @@ ${axisSection}
|
|
|
16019
16162
|
CONTRACT_SURFACE,
|
|
16020
16163
|
DEFENSIVE_CODING,
|
|
16021
16164
|
GESTURE_ROUTING,
|
|
16165
|
+
TERMINAL_ACTIONS,
|
|
16022
16166
|
INTERACTIVE_TRAITS,
|
|
16023
16167
|
ANTI_PATTERNS,
|
|
16024
16168
|
CROSS_REFERENCE_RULES,
|
|
@@ -17077,7 +17221,7 @@ var HOST_CLICK_TAGS = /* @__PURE__ */ new Set(["button", "a", "div", "span", "li
|
|
|
17077
17221
|
var HOST_CHANGE_TAGS = /* @__PURE__ */ new Set(["select", "input", "textarea"]);
|
|
17078
17222
|
function findWiring(input) {
|
|
17079
17223
|
const { sourceCode, hookName, hookArg } = input;
|
|
17080
|
-
const sf =
|
|
17224
|
+
const sf = ts4.createSourceFile("Component.tsx", sourceCode, ts4.ScriptTarget.Latest, true, ts4.ScriptKind.TSX);
|
|
17081
17225
|
const hookBindings = findHookBindings(sf, hookName, hookArg);
|
|
17082
17226
|
if (hookBindings.length === 0) {
|
|
17083
17227
|
return {
|
|
@@ -17100,7 +17244,7 @@ function findWiring(input) {
|
|
|
17100
17244
|
const customPropElements = [];
|
|
17101
17245
|
const submitButton = { found: false };
|
|
17102
17246
|
function visit(node) {
|
|
17103
|
-
if (
|
|
17247
|
+
if (ts4.isJsxAttribute(node) && node.initializer) {
|
|
17104
17248
|
const attrName = node.name.getText(sf);
|
|
17105
17249
|
const initRefs = findReferencedNames(node.initializer, callableNames);
|
|
17106
17250
|
if (initRefs.size > 0) {
|
|
@@ -17138,19 +17282,19 @@ function findWiring(input) {
|
|
|
17138
17282
|
}
|
|
17139
17283
|
}
|
|
17140
17284
|
}
|
|
17141
|
-
if (
|
|
17285
|
+
if (ts4.isJsxOpeningLikeElement(node)) {
|
|
17142
17286
|
const tag = getTagName(node);
|
|
17143
17287
|
if ((tag ?? "").toLowerCase() === "button") {
|
|
17144
17288
|
const typeAttr = node.attributes.properties.find(
|
|
17145
|
-
(p) =>
|
|
17289
|
+
(p) => ts4.isJsxAttribute(p) && p.name.getText(sf) === "type"
|
|
17146
17290
|
);
|
|
17147
|
-
if (typeAttr &&
|
|
17291
|
+
if (typeAttr && ts4.isJsxAttribute(typeAttr) && typeAttr.initializer) {
|
|
17148
17292
|
const v = typeAttr.initializer.getText(sf).toLowerCase();
|
|
17149
17293
|
if (v.includes("submit")) submitButton.found = true;
|
|
17150
17294
|
}
|
|
17151
17295
|
}
|
|
17152
17296
|
}
|
|
17153
|
-
|
|
17297
|
+
ts4.forEachChild(node, visit);
|
|
17154
17298
|
}
|
|
17155
17299
|
visit(sf);
|
|
17156
17300
|
if (sawClickOnHost) {
|
|
@@ -17187,32 +17331,32 @@ function findWiring(input) {
|
|
|
17187
17331
|
function findHookBindings(sf, hookName, hookArg) {
|
|
17188
17332
|
const bindings = [];
|
|
17189
17333
|
function visit(node) {
|
|
17190
|
-
if (
|
|
17334
|
+
if (ts4.isVariableDeclaration(node) && node.initializer && ts4.isCallExpression(node.initializer)) {
|
|
17191
17335
|
const call = node.initializer;
|
|
17192
17336
|
const callee = call.expression.getText(sf);
|
|
17193
17337
|
if (callee !== hookName) {
|
|
17194
|
-
|
|
17338
|
+
ts4.forEachChild(node, visit);
|
|
17195
17339
|
return;
|
|
17196
17340
|
}
|
|
17197
17341
|
const firstArg = call.arguments[0];
|
|
17198
|
-
if (!firstArg || !
|
|
17199
|
-
|
|
17342
|
+
if (!firstArg || !ts4.isStringLiteral(firstArg)) {
|
|
17343
|
+
ts4.forEachChild(node, visit);
|
|
17200
17344
|
return;
|
|
17201
17345
|
}
|
|
17202
17346
|
if (firstArg.text !== hookArg) {
|
|
17203
|
-
|
|
17347
|
+
ts4.forEachChild(node, visit);
|
|
17204
17348
|
return;
|
|
17205
17349
|
}
|
|
17206
|
-
if (
|
|
17350
|
+
if (ts4.isIdentifier(node.name)) {
|
|
17207
17351
|
bindings.push(node.name.text);
|
|
17208
|
-
} else if (
|
|
17352
|
+
} else if (ts4.isObjectBindingPattern(node.name)) {
|
|
17209
17353
|
for (const elem of node.name.elements) {
|
|
17210
17354
|
const target = elem.name;
|
|
17211
|
-
if (
|
|
17355
|
+
if (ts4.isIdentifier(target)) bindings.push(target.text);
|
|
17212
17356
|
}
|
|
17213
17357
|
}
|
|
17214
17358
|
}
|
|
17215
|
-
|
|
17359
|
+
ts4.forEachChild(node, visit);
|
|
17216
17360
|
}
|
|
17217
17361
|
visit(sf);
|
|
17218
17362
|
return bindings;
|
|
@@ -17220,24 +17364,24 @@ function findHookBindings(sf, hookName, hookArg) {
|
|
|
17220
17364
|
function expandAliases(sf, bindings) {
|
|
17221
17365
|
const all = new Set(bindings);
|
|
17222
17366
|
function visit(node) {
|
|
17223
|
-
if (
|
|
17367
|
+
if (ts4.isVariableDeclaration(node) && node.initializer && ts4.isIdentifier(node.name)) {
|
|
17224
17368
|
const newName = node.name.text;
|
|
17225
17369
|
const init = node.initializer;
|
|
17226
|
-
if (
|
|
17370
|
+
if (ts4.isIdentifier(init) && all.has(init.text)) {
|
|
17227
17371
|
all.add(newName);
|
|
17228
17372
|
}
|
|
17229
|
-
if (
|
|
17373
|
+
if (ts4.isArrowFunction(init) || ts4.isFunctionExpression(init)) {
|
|
17230
17374
|
if (bodyReferences(init.body, all)) {
|
|
17231
17375
|
all.add(newName);
|
|
17232
17376
|
}
|
|
17233
17377
|
}
|
|
17234
|
-
if (
|
|
17378
|
+
if (ts4.isCallExpression(init)) {
|
|
17235
17379
|
if (bodyReferences(init, all)) {
|
|
17236
17380
|
all.add(newName);
|
|
17237
17381
|
}
|
|
17238
17382
|
}
|
|
17239
17383
|
}
|
|
17240
|
-
|
|
17384
|
+
ts4.forEachChild(node, visit);
|
|
17241
17385
|
}
|
|
17242
17386
|
visit(sf);
|
|
17243
17387
|
return all;
|
|
@@ -17246,11 +17390,11 @@ function bodyReferences(body, names) {
|
|
|
17246
17390
|
let found = false;
|
|
17247
17391
|
function walk(n) {
|
|
17248
17392
|
if (found) return;
|
|
17249
|
-
if (
|
|
17393
|
+
if (ts4.isIdentifier(n) && names.has(n.text)) {
|
|
17250
17394
|
found = true;
|
|
17251
17395
|
return;
|
|
17252
17396
|
}
|
|
17253
|
-
|
|
17397
|
+
ts4.forEachChild(n, walk);
|
|
17254
17398
|
}
|
|
17255
17399
|
walk(body);
|
|
17256
17400
|
return found;
|
|
@@ -17258,16 +17402,16 @@ function bodyReferences(body, names) {
|
|
|
17258
17402
|
function findReferencedNames(node, names) {
|
|
17259
17403
|
const found = /* @__PURE__ */ new Set();
|
|
17260
17404
|
function walk(n) {
|
|
17261
|
-
if (
|
|
17405
|
+
if (ts4.isIdentifier(n) && names.has(n.text)) {
|
|
17262
17406
|
found.add(n.text);
|
|
17263
17407
|
}
|
|
17264
|
-
|
|
17408
|
+
ts4.forEachChild(n, walk);
|
|
17265
17409
|
}
|
|
17266
17410
|
walk(node);
|
|
17267
17411
|
return found;
|
|
17268
17412
|
}
|
|
17269
17413
|
function getTagName(el) {
|
|
17270
|
-
if (
|
|
17414
|
+
if (ts4.isJsxOpeningLikeElement(el)) {
|
|
17271
17415
|
return el.tagName.getText();
|
|
17272
17416
|
}
|
|
17273
17417
|
return null;
|
|
@@ -21010,6 +21154,10 @@ interface Props ${tsInterface}
|
|
|
21010
21154
|
if (entry.nextStep) {
|
|
21011
21155
|
line += `
|
|
21012
21156
|
Next-step hint: agent intends to call \`${entry.nextStep}\``;
|
|
21157
|
+
}
|
|
21158
|
+
if (entry.confirm === true) {
|
|
21159
|
+
line += `
|
|
21160
|
+
Author flag: \`confirm\` \u2014 the author marked this action grave enough to confirm before it fires, so a second press is a mistake.`;
|
|
21013
21161
|
}
|
|
21014
21162
|
return line;
|
|
21015
21163
|
}).join("\n");
|