#!/usr/bin/env python3
"""Pure-Python self checks for Yida custom-page scripts.
This file lives under scripts/ because packaged workspace zips include scripts
but may exclude tests/. It intentionally does not touch remote Yida resources.
Usage:
python yida_page_self_check.py
"""
from __future__ import annotations
import json
import re
import shutil
import sys
import tempfile
from argparse import Namespace
from pathlib import Path
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
import yida_custom_page_update as custom_update # noqa: E402
import yida_form_inspector as form_inspector # noqa: E402
import yida_jsx_pipeline as jsx_pipeline # noqa: E402
from yida_jsx_pipeline import lint_check # noqa: E402
from yida_page_compiler import build_page_source, compile_jsx_to_schema # noqa: E402
from yida_page_generate import generate, manifest_path_for # noqa: E402
def _args(template: str, output: Path, spec: str | None = None) -> Namespace:
return Namespace(
template=template,
spec=spec,
output=str(output),
compile=True,
form="FORM-SELF-CHECK",
title=None,
subtitle=None,
brand_name=None,
tagline=None,
item=None,
json=True,
)
def _assert_generated_ok(template: str, output: Path, spec: dict | None = None) -> None:
spec_path = None
if spec is not None:
spec_path = output.with_suffix(".spec.json")
spec_path.write_text(json.dumps(spec, ensure_ascii=False), encoding="utf-8")
result = generate(_args(template, output, str(spec_path) if spec_path else None))
if not result.get("ok"):
raise AssertionError(f"{template} generation failed: {result}")
manifest = manifest_path_for(output)
if not output.exists() or not manifest.exists():
raise AssertionError(f"{template} did not write output/manifest")
manifest_data = json.loads(manifest.read_text(encoding="utf-8"))
if manifest_data.get("template") != template:
raise AssertionError(f"{template} manifest template mismatch: {manifest_data}")
source = output.read_text(encoding="utf-8")
lint = lint_check(source, filename=str(output))
if not lint.get("ok"):
raise AssertionError(f"{template} lint failed: {lint}")
compiled = compile_jsx_to_schema(source, form_uuid="FORM-SELF-CHECK")
if not compiled.get("ok") or not compiled.get("schema"):
raise AssertionError(f"{template} compile failed: {compiled.get('errors')}")
def check_page_generator() -> None:
tmp = Path(tempfile.mkdtemp(prefix="dws-yida-self-check-"))
try:
_assert_generated_ok("product-homepage", tmp / "home.jsx", {
"title": "解决方案中心",
"subtitle": "稳定生成的宜搭自定义页",
"features": [
{"title": "客户洞察", "text": "聚合客户和拜访信息。"},
{"title": "方案资产", "text": "沉淀可复用材料。"},
],
"metrics": [
{"value": "12", "label": "客户"},
{"value": "5", "label": "方案"},
],
})
_assert_generated_ok("todo-mvc", tmp / "todo.jsx", {
"title": "交付待办",
"todos": [
{"content": "确认字段", "done": True},
{"content": "发布页面", "done": False},
],
})
finally:
shutil.rmtree(tmp, ignore_errors=True)
def check_missing_custom_state_guard() -> None:
bad_source = """export function renderJsx() {
return
{_customState.title}
;
}
"""
lint = lint_check(bad_source, filename="bad.jsx")
rules = {item.get("rule") for item in lint.get("errors", [])}
if "missing-custom-state" not in rules:
raise AssertionError(f"missing-custom-state guard did not fire: {lint}")
def check_compiler_injects_custom_state() -> None:
source = """export function renderJsx() {
return React.createElement('div', null, _customState.title || 'ok');
}
"""
result = build_page_source(source)
if "var _customState = {};" not in result.get("code", ""):
raise AssertionError("compiler did not inject missing _customState store")
def check_modern_hooks_authoring() -> None:
source = """import React, { useState, useEffect } from 'react';
export default function Page() {
var [count, setCount] = useState(0);
useEffect(function() {
setCount(1);
}, []);
return
{count}
;
}
"""
lint = lint_check(source, filename="modern.jsx")
if not lint.get("ok"):
raise AssertionError(f"modern hooks authoring lint failed: {lint}")
result = compile_jsx_to_schema(source, form_uuid="FORM-SELF-CHECK")
if not result.get("ok") or not result.get("schema"):
raise AssertionError(f"modern hooks authoring compile failed: {result.get('errors')}")
compiled = result.get("compiled_code", "")
if "useState" in compiled or "useEffect" in compiled or "import React" in compiled:
raise AssertionError("modern hooks authoring was not lowered before publish")
if "_customState" not in compiled or "exports.renderJsx" not in compiled:
raise AssertionError("compiled hooks output is missing runtime contract")
def check_modern_helper_functions_are_bound() -> None:
source = """import React, { useState, useEffect } from 'react';
export default function Page() {
var [dataList, setDataList] = useState([]);
useEffect(function() {
loadData();
}, []);
var loadData = function() {
setDataList([{ id: '1', title: 'ok' }]);
};
var renderListItem = function(item) {
return ;
};
var renderListView = function() {
if (!dataList || dataList.length === 0) {
return
;
}
"""
result = compile_jsx_to_schema(source, form_uuid="FORM-SELF-CHECK", minify=False)
if not result.get("ok") or not result.get("schema"):
raise AssertionError(f"modern helper compile failed: {result.get('errors')}")
intermediate = result.get("intermediate_code", "")
required = [
"export function loadData()",
"export function renderListView()",
"this.loadData();",
"this.renderListView()",
"this.renderListItem(item)",
]
missing = [text for text in required if text not in intermediate]
if missing:
raise AssertionError(f"modern helper functions were not bound: missing={missing}\n{intermediate}")
def check_modern_functional_state_updater() -> None:
source = """import React, { useState } from 'react';
export default function Page() {
var [items, setItems] = useState([]);
function addItem() {
setItems(function(prev) {
return prev.concat(['next']);
});
}
return ;
}
"""
result = build_page_source(source)
code = result.get("code", "")
if result.get("errors"):
raise AssertionError(f"functional updater compile failed: {result.get('errors')}")
if "setCustomState({ 'items': function(prev)" not in code:
raise AssertionError(f"functional updater call was not preserved:\n{code}")
if 'if (typeof value === "function")' not in code:
raise AssertionError(f"setCustomState does not execute functional updaters:\n{code}")
def check_modern_render_derived_vars_stay_in_render() -> None:
source = """import React, { useState } from 'react';
export default function Page() {
var [items, setItems] = useState([]);
function toRows(list) {
return list.map(function(item) { return { label: item, value: item.length }; });
}
var rows = toRows(items);
var maxValue = Math.max.apply(null, rows.map(function(row) { return row.value; }).concat([1]));
return
{rows.map((row, idx) => {row.label}:{maxValue})}
;
}
"""
result = compile_jsx_to_schema(source, form_uuid="FORM-SELF-CHECK", minify=False)
if not result.get("ok") or not result.get("schema"):
raise AssertionError(f"modern derived vars compile failed: {result.get('errors')}")
compiled = result.get("compiled_code", "")
render_idx = compiled.find("function renderJsx()")
rows_idx = compiled.find("var rows =")
max_idx = compiled.find("var maxValue =")
if render_idx < 0 or rows_idx < render_idx or max_idx < render_idx:
raise AssertionError(f"derived render vars escaped renderJsx:\n{compiled}")
module_prefix = compiled[:render_idx]
if "var maxValue =" in module_prefix or "var rows =" in module_prefix:
raise AssertionError(f"derived render vars leaked to module scope:\n{compiled}")
def check_jsx_arrow_expression_body_is_transformed() -> None:
source = """export function renderJsx() {
var names = ['A'];
return ;
}
"""
result = compile_jsx_to_schema(source, form_uuid="FORM-SELF-CHECK", minify=False)
if not result.get("ok") or not result.get("schema"):
raise AssertionError(f"arrow JSX expression compile failed: {result.get('errors')}")
compiled = result.get("compiled_code", "")
if "