#!/usr/bin/env python3 """yida_page_compiler.py — 纯 Python 宜搭自定义页面编译管线。 两块能力按 region 分段维护: 1) [region: page_compat] 现代 JSX (export default + useState/useEffect) → 宜搭 runtime 兼容格式 (export function renderJsx + _customState + didMount/didUnmount) 2) [region: compiler] page-compat → JSX→createElement → bind-this → ESM→CJS → minify 的端到端编译。 公开 API(被 yida_custom_page_update.py 直接 import): - compile_jsx(jsx_source, modern=None, minify=True) -> dict - compile_jsx_to_schema(jsx_source, form_uuid=None, ...) -> dict - build_page_source(source_code) -> dict - ensure_runtime_contract(source_code) -> dict - minify_js(source) -> str 外部依赖:无(纯 Python 标准库)。JSX 转换从 yida_jsx_pipeline.transform_jsx; schema 外壳从 yida_page_schema.build_schema_content。 """ import json import re import sys 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)) # JSX 转换与 schema 外壳从其它聚合文件 import(避免重复实现) from yida_jsx_pipeline import transform_jsx from yida_page_schema import build_schema_content # =========================================================================== # region: page_compat —— modern JSX → 宜搭 runtime 格式 # =========================================================================== # # 支持的转换: # - export default function Page() → export function renderJsx() # - var [x, setX] = useState(init) → this.getCustomState('x') / this.setCustomState({x: val}) # - setX(function(prev) { ... }) → setCustomState calls updater with previous value # - useEffect(fn, []) → didMount 生命周期 # - useEffect cleanup return → didUnmount 生命周期 # - 移除 React/react-dom import # - const/let → var # - 补齐 runtime contract exports # --------------------------------------------------------------------------- _REMOVABLE_IMPORTS = frozenset(['react', 'react-dom', 'lodash', 'moment']) _RUNTIME_EXPORTS = { 'getCustomState': ( 'export function getCustomState(key) {\n' ' if (typeof _customState === "undefined") {\n' ' return key ? undefined : {};\n' ' }\n' ' if (key) {\n' ' return _customState[key];\n' ' }\n' ' return Object.assign({}, _customState);\n' '}' ), 'setCustomState': ( 'export function setCustomState(newState) {\n' ' if (typeof _customState === "undefined") {\n' ' return;\n' ' }\n' ' Object.keys(newState || {}).forEach(function(key) {\n' ' var value = newState[key];\n' ' if (typeof value === "function") {\n' ' value = value(_customState[key]);\n' ' }\n' ' _customState[key] = value;\n' ' });\n' ' this.forceUpdate();\n' '}' ), 'forceUpdate': ( 'export function forceUpdate() {\n' ' this.setState({ timestamp: new Date().getTime() });\n' '}' ), 'didMount': 'export function didMount() {}', 'didUnmount': 'export function didUnmount() {}', } _TIMESTAMP_NODE = ( "React.createElement('div', {style: {display: 'none'}}, " "this.state && this.state.timestamp)" ) _CUSTOM_STATE_DECL_RE = re.compile(r'\bvar\s+_customState\b') def _build_render_jsx_with_marker(render_body): """把 render_body 包进 IIFE,外层加 timestamp marker。 产出: export function renderJsx() { var __yidaTs = this.state && this.state.timestamp; var __yidaContent = (function() { ...原 render_body... }).call(this); return React.createElement('div', null, React.createElement('div', {style: {display: 'none'}}, __yidaTs), __yidaContent ); } 作用:__yidaTs 本体在 IIFE 之外被读取使 setState({timestamp}) 能被 React diff、 隐藏 div 把 __yidaTs 放到 JSX 树里使其参与重渲染;原 content 不动。 多一层匿名
包裹,不影响布局,仅 body > div > xxx 选择器会失效。 """ if not render_body: render_body = ' return null;' indented = '\n'.join( (' ' + line) if line else line for line in render_body.split('\n') ) return ( 'export function renderJsx() {\n' ' var __yidaTs = this.state && this.state.timestamp;\n' ' var __yidaContent = (function() {\n' + indented + '\n' ' }).call(this);\n' " return React.createElement('div', null,\n" " React.createElement('div', {style: {display: 'none'}}, __yidaTs),\n" ' __yidaContent\n' ' );\n' '}' ) def build_page_source(source_code): """将现代 JSX 页面源码转换为宜搭 runtime 兼容格式。 Args: source_code: 现代格式的 JSX 源码(export default function Page) Returns: dict: { 'code': str, 转换后的代码 'lint': {'errors': [], 'warnings': []}, 'fixes': [str], 应用的修复描述 'errors': [{'code': str, 'message': str}], } """ if not _should_transform(source_code): result = ensure_runtime_contract(source_code) return { 'code': result['code'], 'lint': {'errors': [], 'warnings': []}, 'fixes': result['fixes'], 'errors': [], } fixes = [] errors = [] # Step 0: 剥离 JSX 表达式注释 {/* ... */} source_code = _strip_jsx_comments(source_code, fixes) lines = source_code.split('\n') # Step 1: 移除 React 等 import lines = _remove_imports(lines, fixes, errors) # Step 2: const/let → var lines = _fix_variable_declarations(lines, fixes) # Step 3: 提取 Page 函数体 page_info = _extract_page_function(lines, errors) if page_info is None: code = '\n'.join(lines) result = ensure_runtime_contract(code) return { 'code': result['code'], 'lint': {'errors': errors, 'warnings': []}, 'fixes': fixes + result['fixes'], 'errors': errors, } # Step 4: 从函数体中提取 useState 和 useEffect body_lines = page_info['body_lines'] state_info = _extract_use_state(body_lines, fixes, errors) effect_info = _extract_use_effect(body_lines, fixes, errors) # Step 5: 构建 renderJsx 函数 remaining_body = _remove_hooks_from_body(body_lines, state_info, effect_info) # Step 5.5: 分离函数体为三类 separated = _separate_body_parts(remaining_body, state_info) # Step 6: 构建输出 output_parts = [] output_parts.append('\n'.join(page_info['before_lines'])) # _customState 初始化 state_inits = [] for st in state_info['states']: state_inits.append(f" {st['name']}: {st['init']},") if state_inits: output_parts.append('var _customState = {\n' + '\n'.join(state_inits) + '\n};') else: output_parts.append('var _customState = {};') # 模块顶层变量 if separated['top_level_vars']: top_vars = _replace_state_references(separated['top_level_vars'], state_info) output_parts.append(top_vars) # 纯工具函数 for fn_code in separated['utility_functions']: output_parts.append(fn_code.strip()) # 导出函数声明(变为组件方法) function_names = separated['function_names'] for fn_code in separated['functions']: fn_transformed = _replace_state_references(fn_code, state_info) fn_transformed = _add_bind_this_to_then_catch(fn_transformed) fn_transformed = _replace_fn_calls_with_this(fn_transformed, function_names) output_parts.append('export ' + fn_transformed.strip()) # renderJsx render_body = _replace_state_references(separated['render_body'], state_info, receiver='self') render_body = _replace_fn_calls_with_this(render_body, function_names) if ('self.getCustomState' in render_body or 'self.setCustomState' in render_body) \ and not re.search(r'\bvar\s+self\s*=\s*this\b', render_body): render_body = 'var self = this;\n' + render_body render_fn = _build_render_jsx_with_marker(render_body) output_parts.append(render_fn) fixes.append('Converted export default function Page → export function renderJsx (with re-render marker)') # didMount/didUnmount mount_code = _replace_state_references(effect_info['mount_code'], state_info) if effect_info['mount_code'] else '' unmount_code = _replace_state_references(effect_info['unmount_code'], state_info) if effect_info['unmount_code'] else '' if mount_code: mount_code = _replace_fn_calls_with_this(mount_code, function_names) mount_code = _add_bind_this_to_then_catch(mount_code) if unmount_code: unmount_code = _replace_fn_calls_with_this(unmount_code, function_names) unmount_code = _add_bind_this_to_then_catch(unmount_code) if mount_code: mount_fn = 'export function didMount() {\n' + mount_code + '\n}' output_parts.append(mount_fn) fixes.append('Extracted useEffect(fn, []) → didMount') else: output_parts.append('export function didMount() {}') if unmount_code: unmount_fn = 'export function didUnmount() {\n' + unmount_code + '\n}' output_parts.append(unmount_fn) fixes.append('Extracted useEffect cleanup → didUnmount') else: output_parts.append('export function didUnmount() {}') if page_info['after_lines']: output_parts.append('\n'.join(page_info['after_lines'])) code = '\n\n'.join(p for p in output_parts if p.strip()) result = ensure_runtime_contract(code) return { 'code': result['code'], 'lint': {'errors': errors, 'warnings': []}, 'fixes': fixes + result['fixes'], 'errors': errors, } def ensure_runtime_contract(source_code): """确保代码包含宜搭 runtime 必需的 export 函数。 Args: source_code: 已转换的源码 Returns: dict: {'code': str, 'fixes': [str]} """ fixes = [] append_parts = [] exported_names = set(re.findall(r'export\s+function\s+(\w+)', source_code)) if '_customState' in source_code and not _CUSTOM_STATE_DECL_RE.search(source_code): append_parts.insert(0, 'var _customState = {};') fixes.append('Inserted missing _customState store') if 'getCustomState' not in exported_names or 'setCustomState' not in exported_names: if '_customState' not in source_code: append_parts.insert(0, 'var _customState = {};') fixes.append('Inserted default _customState store') for name, body in _RUNTIME_EXPORTS.items(): if name not in exported_names: append_parts.append(body) fixes.append(f'Inserted missing export function {name}') if not append_parts: return {'code': source_code, 'fixes': fixes} return { 'code': source_code.rstrip() + '\n\n' + '\n\n'.join(append_parts) + '\n', 'fixes': fixes, } def _should_transform(source_code): """判断是否需要做 modern → yida 转换""" if re.search(r'export\s+function\s+renderJsx\s*\(', source_code): return False return bool(re.search(r'export\s+default\b', source_code)) def _strip_jsx_comments(source_code, fixes): """剥离 JSX 表达式容器注释 {/* ... */}(避免 ,, 双逗号)。""" if not source_code or '/*' not in source_code: return source_code original = source_code source_code = re.sub( r'(?m)^[ \t]*\{\s*/\*[\s\S]*?\*/\s*\}[ \t]*\r?\n', '', source_code, ) source_code = re.sub( r'(>)\s*\{\s*/\*[\s\S]*?\*/\s*\}\s*(<)', r'\1\2', source_code, ) if source_code != original: fixes.append('Stripped JSX expression comments (avoid ",," double-comma)') return source_code def _remove_imports(lines, fixes, errors): """移除已知的 removable imports,对未知的报错""" result = [] for line in lines: match = re.match(r"^\s*import\s+.*?from\s+['\"]([^'\"]+)['\"]", line) if not match: match = re.match(r"^\s*import\s+['\"]([^'\"]+)['\"]", line) if match: module_name = match.group(1).lower() if module_name in _REMOVABLE_IMPORTS: fixes.append(f'Removed {module_name} import') continue else: errors.append({ 'code': 'UNSUPPORTED_IMPORT', 'message': f'Unsupported import "{match.group(1)}". ' 'Use this.utils.loadScript for external libraries.', }) result.append(line) return result def _fix_variable_declarations(lines, fixes): """const/let → var""" result = [] pattern = re.compile(r'^(\s*)(const|let)\b(.*)$') changed = False for line in lines: m = pattern.match(line) if m: result.append(f'{m.group(1)}var{m.group(3)}') changed = True else: result.append(line) if changed: fixes.append('Replaced const/let with var') return result def _extract_page_function(lines, errors): """提取 export default function Page() { ... } 的位置和内容""" pattern = re.compile(r'^(\s*)export\s+default\s+function\s+\w*\s*\([^)]*\)\s*\{?\s*$') start_idx = None for i, line in enumerate(lines): if pattern.match(line): start_idx = i break if re.match(r'^\s*export\s+default\s+function\b', line): start_idx = i break if start_idx is None: errors.append({ 'code': 'NO_DEFAULT_EXPORT', 'message': 'Cannot find export default function Page().', }) return None brace_start = None text_from_start = '\n'.join(lines[start_idx:]) for i, ch in enumerate(text_from_start): if ch == '{': brace_start = i break if brace_start is None: errors.append({'code': 'PARSE_ERROR', 'message': 'Cannot find opening brace of Page function.'}) return None depth = 0 all_text = '\n'.join(lines) char_pos = len('\n'.join(lines[:start_idx])) + (1 if start_idx > 0 else 0) + brace_start end_char = None pos = char_pos while pos < len(all_text): ch = all_text[pos] if ch in ('"', "'", '`'): pos = _skip_string_in_source(all_text, pos) + 1 continue if ch == '{': depth += 1 elif ch == '}': depth -= 1 if depth == 0: end_char = pos break pos += 1 if end_char is None: errors.append({'code': 'PARSE_ERROR', 'message': 'Cannot find closing brace of Page function.'}) return None body_content = all_text[char_pos + 1:end_char] end_line = all_text[:end_char + 1].count('\n') before_lines = lines[:start_idx] after_lines = lines[end_line + 1:] if end_line + 1 < len(lines) else [] body_lines = body_content.split('\n') return { 'before_lines': before_lines, 'body_lines': body_lines, 'after_lines': after_lines, } def _skip_string_in_source(text, pos): """跳过字符串字面量""" quote = text[pos] pos += 1 while pos < len(text): ch = text[pos] if ch == '\\': pos += 2 continue if ch == quote: return pos pos += 1 return pos def _extract_use_state(body_lines, fixes, errors): """提取所有 useState 声明(支持跨行初值)""" states = [] body_text = '\n'.join(body_lines) decl_pattern = re.compile( r'(?m)^([ \t]*)var\s+\[(\w+)\s*,\s*(\w+)\]\s*=\s*useState\s*\(' ) for m in decl_pattern.finditer(body_text): name = m.group(2) setter = m.group(3) paren_start = m.end() - 1 depth = 0 pos = paren_start while pos < len(body_text): ch = body_text[pos] if ch in ('"', "'", '`'): pos = _skip_string_in_source(body_text, pos) + 1 continue if ch == '(': depth += 1 elif ch == ')': depth -= 1 if depth == 0: break pos += 1 if depth != 0: errors.append({ 'code': 'UNSUPPORTED_USE_STATE', 'message': f'Unbalanced parentheses in useState({name}) declaration.', }) continue init_text = body_text[paren_start + 1:pos].strip() decl_end = pos + 1 while decl_end < len(body_text) and body_text[decl_end] in ' \t': decl_end += 1 if decl_end < len(body_text) and body_text[decl_end] == ';': decl_end += 1 states.append({ 'name': name, 'setter': setter, 'init': init_text, 'raw_text': body_text[m.start():decl_end], }) for line in body_lines: hook_match = re.search(r'\buse([A-Z]\w*)\b', line) if hook_match: hook_name = 'use' + hook_match.group(1) if hook_name not in ('useState', 'useEffect'): errors.append({ 'code': 'UNSUPPORTED_HOOK', 'message': f'{hook_name} is not supported.', }) return {'states': states} def _extract_use_effect(body_lines, fixes, errors): """提取 useEffect(fn, []) 调用""" mount_code = '' unmount_code = '' body_text = '\n'.join(body_lines) effect_pattern = re.compile(r'useEffect\s*\(\s*function\s*\(\)\s*\{') for match in effect_pattern.finditer(body_text): start = match.end() depth = 1 pos = start while pos < len(body_text) and depth > 0: ch = body_text[pos] if ch in ('"', "'", '`'): pos = _skip_string_in_source(body_text, pos) + 1 continue if ch == '{': depth += 1 elif ch == '}': depth -= 1 pos += 1 fn_body = body_text[start:pos - 1] remaining_after_fn = body_text[pos:].lstrip() if not remaining_after_fn.startswith(','): errors.append({'code': 'UNSUPPORTED_EFFECT_DEPS', 'message': 'useEffect must have [], as second arg.'}) continue deps_start = body_text.index(',', pos) + 1 deps_text = body_text[deps_start:].lstrip() if not deps_text.startswith('[]') and not deps_text.startswith('[ ]'): errors.append({'code': 'UNSUPPORTED_EFFECT_DEPS', 'message': 'Only useEffect(fn, []) is supported.'}) continue mount_lines, unmount_lines = _split_effect_body(fn_body) if mount_lines: mount_code += mount_lines + '\n' if unmount_lines: unmount_code += unmount_lines + '\n' return { 'mount_code': mount_code.strip(), 'unmount_code': unmount_code.strip(), } def _split_effect_body(fn_body): """将 effect 函数体分为 mount 代码和 unmount(return 的 cleanup 函数)""" lines = fn_body.strip().split('\n') mount_lines = [] unmount_lines = [] return_idx = None for i in range(len(lines) - 1, -1, -1): if re.match(r'^\s*return\s+function\b', lines[i]): return_idx = i break if re.match(r'^\s*return\s*\(\s*function\b', lines[i]): return_idx = i break if return_idx is None: return '\n'.join(lines), '' mount_lines = lines[:return_idx] return_text = '\n'.join(lines[return_idx:]) brace_match = return_text.find('{') if brace_match == -1: return '\n'.join(mount_lines), '' depth = 0 end_pos = None for i in range(brace_match, len(return_text)): if return_text[i] == '{': depth += 1 elif return_text[i] == '}': depth -= 1 if depth == 0: end_pos = i break if end_pos is not None: cleanup_body = return_text[brace_match + 1:end_pos] unmount_lines = cleanup_body.strip() return '\n'.join(mount_lines), unmount_lines def _remove_hooks_from_body(body_lines, state_info, effect_info): """从函数体中移除 useState 和 useEffect 声明行""" body_text = '\n'.join(body_lines) for st in state_info['states']: raw = st.get('raw_text') or st.get('line') or '' if raw: body_text = body_text.replace(raw, '') effect_pattern = re.compile(r'\s*useEffect\s*\(') while True: m = effect_pattern.search(body_text) if not m: break start = m.start() paren_start = body_text.index('(', m.start()) depth = 0 pos = paren_start while pos < len(body_text): ch = body_text[pos] if ch in ('"', "'", '`'): pos = _skip_string_in_source(body_text, pos) + 1 continue if ch == '(': depth += 1 elif ch == ')': depth -= 1 if depth == 0: break pos += 1 end = pos + 1 if end < len(body_text) and body_text[end] == ';': end += 1 body_text = body_text[:start] + body_text[end:] body_text = re.sub(r'\n{3,}', '\n\n', body_text) return body_text.strip() def _replace_state_references(body_text, state_info, receiver='this'): """将 state 变量引用替换为 this.getCustomState/setCustomState(跳过字符串)。""" for st in state_info['states']: name = st['name'] setter = st['setter'] body_text = _replace_setter_calls(body_text, setter, name, receiver=receiver) body_text = _replace_state_reads(body_text, name, receiver=receiver) return body_text def _replace_state_reads(text, state_name, receiver='this'): """替换裸变量名引用为 this.getCustomState('name'),跳过字符串内容和对象属性 key。""" pattern = re.compile(r'(?': prev_prev = preceding[-2] if len(preceding) >= 2 else '' if prev_prev not in ('=', '!', '<', '>'): result.append(text[pos:m.end()]) pos = m.end() continue rest_after = text[after_end:].lstrip() if rest_after and rest_after[0] == ':' and not rest_after.startswith('::'): if preceding and preceding[-1] in ('{', ',', '\n'): result.append(text[pos:m.end()]) pos = m.end() continue result.append(f"{receiver}.getCustomState('{state_name}')") pos = m.end() else: result.append(ch) pos += 1 return ''.join(result) def _replace_setter_calls(text, setter_name, state_name, receiver='this'): """替换 setX(value) 为 this.setCustomState({'x': value}),跳过字符串内容。""" setter_pattern = re.compile(r'(? 0: if text[pos] == '{': depth += 1 elif text[pos] == '}': depth -= 1 pos += 1 continue pos += 1 return len(text) - 1 while pos < len(text): ch = text[pos] if ch == '\\': pos += 2 continue if ch == quote: return pos pos += 1 return len(text) - 1 def _consume_var_declaration(lines, start_index): """从 lines[start_index] 起读一条完整的 var 声明,允许初值跨多行。 跟踪 {} [] () 括号深度(跳过字符串、模板字符串、单/多行注释), 括号深度归零的那一行视为声明结束。返回 (end_index_inclusive, full_text)。 """ depth = 0 in_string = False string_quote = '' n = len(lines) end = start_index i = start_index while i < n: line = lines[i] j = 0 L = len(line) while j < L: ch = line[j] if in_string: if string_quote == '`': if ch == '\\' and j + 1 < L: j += 2 continue if ch == '`': in_string = False j += 1 continue if ch == '\\' and j + 1 < L: j += 2 continue if ch == string_quote: in_string = False j += 1 continue if ch == '/' and j + 1 < L and line[j + 1] == '/': break if ch == '/' and j + 1 < L and line[j + 1] == '*': end_block = line.find('*/', j + 2) if end_block == -1: j = L break j = end_block + 2 continue if ch in ('"', "'", '`'): in_string = True string_quote = ch j += 1 continue if ch in '{[(': depth += 1 elif ch in '}])': depth -= 1 j += 1 if depth <= 0: end = i break i += 1 else: end = n - 1 return end, '\n'.join(lines[start_index:end + 1]) def _var_function_to_declaration(decl_text): """Convert `var name = function(args) { ... };` to `function name(args) { ... }`.""" header = re.match(r'^\s*var\s+(\w+)\s*=\s*function\s*(\([^)]*\))\s*\{', decl_text, re.S) if not header: return None, None name = header.group(1) args = header.group(2) open_brace = decl_text.find('{', header.start()) close_brace = decl_text.rfind('}') if open_brace < 0 or close_brace <= open_brace: return None, None body = decl_text[open_brace + 1:close_brace] return name, f"function {name}{args} {{{body}\n}}" def _extract_declared_var_names(decl_text): """Return declared identifiers from a `var ...` declaration. The modern authoring splitter needs these names to keep chained render derivations together, e.g. `filtered` followed by `amount = filtered.reduce(...)`. This parser only handles normal identifiers because useState destructuring is removed before this phase. """ m = re.match(r'^\s*var\s+([\s\S]*)$', decl_text.strip()) if not m: return [] tail = m.group(1) names = [] pos = 0 n = len(tail) while pos < n: while pos < n and tail[pos].isspace(): pos += 1 ident = re.match(r'[A-Za-z_$][\w$]*', tail[pos:]) if ident: names.append(ident.group(0)) pos += len(ident.group(0)) depth = 0 while pos < n: ch = tail[pos] if ch in ('"', "'", '`'): pos = _find_string_end(tail, pos) + 1 continue if ch in '([{': depth += 1 elif ch in ')]}': depth -= 1 elif ch == ',' and depth == 0: pos += 1 break pos += 1 else: break return names def _references_any_identifier(text, names): """Whether text references any identifier in names outside strings.""" if not names: return False clean_parts = [] pos = 0 while pos < len(text): ch = text[pos] if ch in ('"', "'", '`'): end = _find_string_end(text, pos) clean_parts.append(' ') pos = end + 1 else: clean_parts.append(ch) pos += 1 clean = ''.join(clean_parts) return any(re.search(r'\b' + re.escape(name) + r'\b', clean) for name in names) def _separate_body_parts(body_text, state_info): """将函数体分离为:需导出的方法、模块级工具函数、顶层常量、render 部分。 分类规则: - 函数内含 setState/getState 调用 → 导出为组件方法(用 this.xxx() 调用) - 纯工具函数(不依赖 state)→ 模块级普通函数(直接调用) - var 声明依赖 state → 放入 renderJsx - var 声明不依赖 state → 模块顶层常量 - return 语句及之后 → render_body """ lines = body_text.split('\n') exported_functions = [] exported_fn_names = [] utility_functions = [] utility_fn_names = [] top_level_vars = [] render_lines = [] render_var_names = set() state_setters = set(st['setter'] for st in state_info['states']) state_names = set(st['name'] for st in state_info['states']) i = 0 found_return = False while i < len(lines): line = lines[i] stripped = line.strip() if found_return: render_lines.append(line) i += 1 continue if re.match(r'^\s*return\s*[\(]', stripped) or stripped == 'return (' or stripped == 'return(': found_return = True render_lines.append(line) i += 1 continue fn_match = re.match(r'^(\s*)function\s+(\w+)\s*\(', line) if fn_match: fn_name = fn_match.group(2) fn_start = i brace_depth = 0 fn_end = i for j in range(i, len(lines)): for ch in lines[j]: if ch == '{': brace_depth += 1 elif ch == '}': brace_depth -= 1 if brace_depth == 0 and j > i: fn_end = j break if brace_depth == 0 and '{' in lines[j]: fn_end = j break fn_code = '\n'.join(lines[fn_start:fn_end + 1]) uses_state = ( any(s + '(' in fn_code for s in state_setters) or any(re.search(r'\b' + re.escape(sn) + r'\b', fn_code) for sn in state_names) or bool(re.search(r'\bthis\.', fn_code)) ) if uses_state: exported_functions.append(fn_code) exported_fn_names.append(fn_name) else: utility_functions.append(fn_code) utility_fn_names.append(fn_name) i = fn_end + 1 continue if re.match(r'^\s*var\s+\w+\s*=\s*function\s*\(', line): decl_end, decl_text = _consume_var_declaration(lines, i) fn_name, fn_code = _var_function_to_declaration(decl_text) if fn_name and fn_code: exported_functions.append(fn_code) exported_fn_names.append(fn_name) else: render_lines.extend(lines[i:decl_end + 1]) i = decl_end + 1 continue if stripped.startswith('var ') and not stripped.startswith('var ['): decl_end, decl_text = _consume_var_declaration(lines, i) decl_names = _extract_declared_var_names(decl_text) uses_state_var = _references_any_identifier(decl_text, state_names) uses_render_var = _references_any_identifier(decl_text, render_var_names) if uses_state_var or uses_render_var: render_lines.extend(lines[i:decl_end + 1]) render_var_names.update(decl_names) else: top_level_vars.extend(lines[i:decl_end + 1]) i = decl_end + 1 continue if stripped: render_lines.append(line) i += 1 return { 'functions': exported_functions, 'function_names': exported_fn_names, 'utility_functions': utility_functions, 'utility_fn_names': utility_fn_names, 'top_level_vars': '\n'.join(top_level_vars), 'render_body': '\n'.join(render_lines).strip(), } def _replace_fn_calls_with_this(code, function_names): """将裸函数调用 xxx() 替换为 this.xxx()(仅对导出为组件方法的函数)。""" for fn_name in function_names: pattern = re.compile(r'(? 0: ch = text[pos] if ch in ('"', "'", '`'): pos = _find_string_end(text, pos) + 1 continue if ch == '{': depth += 1 elif ch == '}': depth -= 1 pos += 1 fn_body = text[body_start:pos - 1] result.append(fn_body) if 'this.' in fn_body: result.append('}.bind(this)') else: result.append('}') return ''.join(result) # =========================================================================== # region: compiler —— page-compat → JSX→createElement → bind-this → ESM→CJS → minify # =========================================================================== def compile_jsx(jsx_source, modern=None, minify=True): """编译 JSX 源码为宜搭 runtime 兼容的 JS。 Args: jsx_source: JSX/JS 源码字符串 modern: True 强制走 page-compat 转换;None 自动判断 minify: 是否压缩(默认 True) Returns: dict: { 'source_code': str, 原始输入 'intermediate_code': str, page-compat 改写后的中间产物 'compiled_code': str, JSX 转换 + minify 后的最终 JS 'lint': {'errors': [], 'warnings': []}, 'ok': bool, 'errors': [{'message': str, ...}], } """ errors = [] # Step 1: page-compat 转换 compat_result = build_page_source(jsx_source) intermediate_code = compat_result['code'] lint = compat_result.get('lint', {'errors': [], 'warnings': []}) if compat_result.get('errors'): for err in compat_result['errors']: errors.append(err) if lint.get('errors'): return { 'source_code': jsx_source, 'intermediate_code': intermediate_code, 'compiled_code': '', 'lint': lint, 'ok': False, 'errors': errors + lint['errors'], } # Step 2: JSX → React.createElement try: transformed_code = transform_jsx(intermediate_code) except Exception as exc: errors.append({'code': 'JSX_TRANSFORM_ERROR', 'message': str(exc)}) return { 'source_code': jsx_source, 'intermediate_code': intermediate_code, 'compiled_code': '', 'lint': lint, 'ok': False, 'errors': errors, } # Step 2.5: 给含 this. 的匿名函数加 .bind(this) transformed_code = _bind_this_to_anonymous_fns(transformed_code) # Step 2.6: ESM exports → CommonJS(宜搭 runtime 只认 CommonJS) transformed_code = _convert_esm_to_commonjs(transformed_code) # Step 3: minify if minify: compiled_code = minify_js(transformed_code) else: compiled_code = transformed_code ok = len(errors) == 0 return { 'source_code': jsx_source, 'intermediate_code': intermediate_code, 'compiled_code': compiled_code, 'lint': lint, 'ok': ok, 'errors': errors, } def compile_jsx_to_schema(jsx_source, form_uuid=None, existing_data_source=None, modern=None, minify=True): """编译 JSX 源码,可选生成完整自定义页面 schema。""" result = compile_jsx(jsx_source, modern=modern, minify=minify) if form_uuid and result.get('ok'): result['schema'] = build_schema_content( result['source_code'], result['compiled_code'], form_uuid, existing_data_source=existing_data_source, ) else: result['schema'] = None return result def minify_js(source): """简单 JS minifier:移除注释、压缩连续空白、保留字符串内容。""" result = [] pos = 0 length = len(source) while pos < length: ch = source[pos] if ch in ('"', "'"): end = _consume_string(source, pos) result.append(source[pos:end + 1]) pos = end + 1 continue if ch == '`': end = _consume_template(source, pos) result.append(source[pos:end + 1]) pos = end + 1 continue if ch == '/' and pos + 1 < length and source[pos + 1] == '/': while pos < length and source[pos] != '\n': pos += 1 continue if ch == '/' and pos + 1 < length and source[pos + 1] == '*': pos += 2 while pos < length - 1: if source[pos] == '*' and source[pos + 1] == '/': pos += 2 break pos += 1 else: pos = length continue if ch in (' ', '\t', '\r'): while pos < length and source[pos] in (' ', '\t', '\r'): pos += 1 if result and result[-1] and result[-1][-1:].isalnum() or (result and result[-1][-1:] == '_'): if pos < length and (source[pos].isalnum() or source[pos] == '_'): result.append(' ') continue if ch == '\n': while pos < length and source[pos] in ('\n', '\r', ' ', '\t'): pos += 1 if result and result[-1] and result[-1][-1:] not in ('{', '}', ';', ',', '(', '[', '\n', ''): result.append('\n') continue result.append(ch) pos += 1 return ''.join(result) def _consume_string(source, start): """找到字符串字面量的结束位置(含闭合引号)""" quote = source[start] pos = start + 1 while pos < len(source): ch = source[pos] if ch == '\\': pos += 2 continue if ch == quote: return pos pos += 1 return len(source) - 1 def _consume_template(source, start): """找到模板字面量的结束位置(含闭合反引号)""" pos = start + 1 while pos < len(source): ch = source[pos] if ch == '\\': pos += 2 continue if ch == '`': return pos if ch == '$' and pos + 1 < len(source) and source[pos + 1] == '{': pos += 2 depth = 1 while pos < len(source) and depth > 0: if source[pos] == '{': depth += 1 elif source[pos] == '}': depth -= 1 elif source[pos] == '\\': pos += 1 pos += 1 continue pos += 1 return len(source) - 1 def _bind_this_to_anonymous_fns(code): """给含有 this. 引用的匿名函数表达式自动添加 .bind(this)。""" result = [] pos = 0 length = len(code) while pos < length: fn_idx = code.find('function', pos) if fn_idx == -1: result.append(code[pos:]) break if fn_idx > 0 and (code[fn_idx - 1].isalnum() or code[fn_idx - 1] == '_'): result.append(code[pos:fn_idx + 8]) pos = fn_idx + 8 continue after_fn = fn_idx + 8 if after_fn < length and (code[after_fn].isalnum() or code[after_fn] == '_'): pass result.append(code[pos:fn_idx]) line_start = code.rfind('\n', 0, fn_idx) line_start = line_start + 1 if line_start != -1 else 0 prefix = code[line_start:fn_idx].strip() is_declaration = (prefix == '' or prefix == 'export') scan = fn_idx + 8 while scan < length and code[scan] in (' ', '\t'): scan += 1 if scan < length and (code[scan].isalpha() or code[scan] == '_'): while scan < length and (code[scan].isalnum() or code[scan] == '_'): scan += 1 while scan < length and code[scan] in (' ', '\t'): scan += 1 if scan >= length or code[scan] != '(': result.append('function') pos = fn_idx + 8 continue depth = 1 scan += 1 while scan < length and depth > 0: ch = code[scan] if ch == '(': depth += 1 elif ch == ')': depth -= 1 elif ch in ('"', "'", '`'): scan = _skip_str_for_bind(code, scan) scan += 1 paren_end = scan while scan < length and code[scan] in (' ', '\t', '\n', '\r'): scan += 1 if scan >= length or code[scan] != '{': result.append(code[fn_idx:paren_end]) pos = paren_end continue brace_start = scan if is_declaration: result.append(code[fn_idx:brace_start + 1]) pos = brace_start + 1 continue depth = 1 scan += 1 while scan < length and depth > 0: ch = code[scan] if ch in ('"', "'", '`'): scan = _skip_str_for_bind(code, scan) scan += 1 continue if ch == '{': depth += 1 elif ch == '}': depth -= 1 scan += 1 brace_end = scan fn_body = code[brace_start + 1:brace_end - 1] result.append(code[fn_idx:brace_end]) if 'this.' in fn_body: rest = code[brace_end:brace_end + 30] if not rest.lstrip(' \t').startswith('.bind(this)'): result.append('.bind(this)') pos = brace_end return ''.join(result) def _skip_str_for_bind(code, pos): """跳过字符串/模板字面量,返回闭合引号的位置""" quote = code[pos] pos += 1 while pos < len(code): ch = code[pos] if ch == '\\': pos += 2 continue if quote == '`' and ch == '$' and pos + 1 < len(code) and code[pos + 1] == '{': pos += 2 depth = 1 while pos < len(code) and depth > 0: if code[pos] == '{': depth += 1 elif code[pos] == '}': depth -= 1 pos += 1 continue if ch == quote: return pos pos += 1 return pos - 1 def _convert_esm_to_commonjs(code): """将 export function xxx 转换为宜搭 runtime 能识别的格式。 宜搭 runtime(render-engine)期望代码是一个包裹在 function 中的块, 通过 Babel 编译后产出 CommonJS 风格: "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.renderJsx = renderJsx; exports.didMount = didMount; ... function renderJsx() { ... } 但实际上宜搭更简单:它直接 eval 代码后从 exports 对象取函数。 """ lines = code.split('\n') result_lines = ['"use strict";', ''] export_names = [] for line in lines: m = re.match(r'^export\s+function\s+(\w+)', line) if m: fn_name = m.group(1) export_names.append(fn_name) result_lines.append(line.replace('export ', '', 1)) else: result_lines.append(line) if export_names: result_lines.append('') for name in export_names: result_lines.append(f'exports.{name} = {name};') return '\n'.join(result_lines)