146 lines
6.0 KiB
JavaScript
146 lines
6.0 KiB
JavaScript
// Evidence inventory, not a second requirements matrix. Static bindings are not acceptance.
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const ts = require('typescript');
|
|
const root = path.resolve(__dirname, '..');
|
|
const rows = [];
|
|
const controls = [];
|
|
function walk(directory) {
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
const file = path.join(directory, entry.name);
|
|
if (entry.isDirectory()) walk(file);
|
|
else if (entry.name.endsWith('.tsx')) {
|
|
const source = fs.readFileSync(file, 'utf8');
|
|
const tree = ts.createSourceFile(
|
|
file,
|
|
source,
|
|
ts.ScriptTarget.Latest,
|
|
true,
|
|
ts.ScriptKind.TSX,
|
|
);
|
|
function visit(node) {
|
|
const opening = ts.isJsxElement(node)
|
|
? node.openingElement
|
|
: ts.isJsxSelfClosingElement(node)
|
|
? node
|
|
: null;
|
|
if (opening) {
|
|
const tag = opening.tagName.getText(tree);
|
|
const attributes = opening.attributes.properties;
|
|
const names = attributes.map((a) => a.name?.getText(tree));
|
|
const base = {
|
|
file: path.relative(root, file).replace(/\\/g, '/'),
|
|
line:
|
|
tree.getLineAndCharacterOfPosition(
|
|
node.getStart(tree),
|
|
).line + 1,
|
|
component: tag,
|
|
};
|
|
if (tag === 'button' || tag === 'Button') {
|
|
let form = null;
|
|
for (
|
|
let parent = node.parent;
|
|
parent;
|
|
parent = parent.parent
|
|
) {
|
|
if (
|
|
ts.isJsxElement(parent) &&
|
|
['form', 'Form'].includes(
|
|
parent.openingElement.tagName.getText(tree),
|
|
)
|
|
) {
|
|
form = parent.openingElement;
|
|
break;
|
|
}
|
|
}
|
|
const formNames =
|
|
form?.attributes.properties.map((a) =>
|
|
a.name?.getText(tree),
|
|
) ?? [];
|
|
rows.push({
|
|
...base,
|
|
label: ts.isJsxElement(node)
|
|
? node.children
|
|
.map((c) => c.getText(tree))
|
|
.join(' ')
|
|
.replace(/\s+/g, ' ')
|
|
.slice(0, 180)
|
|
: '',
|
|
binding: names.some((n) =>
|
|
['onClick', 'onSelect'].includes(n),
|
|
)
|
|
? 'direct_handler'
|
|
: form && form.tagName.getText(tree) === 'Form'
|
|
? 'inertia_form'
|
|
: formNames.includes('onSubmit')
|
|
? 'form_handler'
|
|
: form &&
|
|
(formNames.includes('action') ||
|
|
formNames.includes('method'))
|
|
? 'native_form_action'
|
|
: names.includes('asChild')
|
|
? 'delegated_child'
|
|
: 'context_or_handler_review',
|
|
functional_demonstration:
|
|
'Not inferred from static binding; use per-journey QA evidence and visual walkthrough',
|
|
});
|
|
}
|
|
if (
|
|
tag === 'select' ||
|
|
/CountrySelect|ProvinceSelect|TerritoryFields|TerritoryLevel|TerritoryField|MapPanel|InvestmentMap|Geography/.test(
|
|
tag,
|
|
)
|
|
) {
|
|
controls.push({
|
|
...base,
|
|
attributes: attributes
|
|
.map((a) => a.getText(tree))
|
|
.join(' ')
|
|
.slice(0, 700),
|
|
visual_verified: false,
|
|
});
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
visit(tree);
|
|
}
|
|
}
|
|
}
|
|
walk(path.join(root, 'resources/js/pages'));
|
|
walk(path.join(root, 'resources/js/components'));
|
|
const file = path.join(
|
|
root,
|
|
'storage/app/private/investmoz-v2/portal-audit/consumer-actions-lote2.json',
|
|
);
|
|
fs.writeFileSync(
|
|
file,
|
|
JSON.stringify(
|
|
{
|
|
captured_at: new Date().toISOString(),
|
|
scope: 'All TSX pages/components; inventory of source controls and action bindings, not rendered DOM or functional completion',
|
|
buttons: rows,
|
|
selectors_and_maps: controls,
|
|
counts: {
|
|
buttons: rows.length,
|
|
selectors_and_maps: controls.length,
|
|
binding_review: rows.filter(
|
|
(r) => r.binding === 'context_or_handler_review',
|
|
).length,
|
|
},
|
|
visual_verified: false,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
console.log(
|
|
JSON.stringify({
|
|
buttons: rows.length,
|
|
selectors_and_maps: controls.length,
|
|
review: rows
|
|
.filter((r) => r.binding === 'context_or_handler_review')
|
|
.map(({ file, line, label }) => ({ file, line, label })),
|
|
}),
|
|
);
|