From 93834f268666285d98c02849788a95f5a373cd6a Mon Sep 17 00:00:00 2001 From: lewiscutey <949124363@qq.com> Date: Fri, 3 Jul 2026 12:32:07 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E8=BD=BB=E5=8D=A1=E7=BC=96?= =?UTF-8?q?=E8=AF=91=E5=99=A8=E6=94=AF=E6=8C=81=20lifecycle=20params=20?= =?UTF-8?q?=E8=A1=A8=E8=BE=BE=E5=BC=8F=E7=BC=96=E8=AF=91=E5=8F=8A=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E8=80=97=E6=97=B6=20Feature=20=E6=A0=A1=E9=AA=8C=20?= =?UTF-8?q?=E9=9C=80=E6=B1=82=E4=B8=80(P0)=EF=BC=9A=E7=BC=96=E8=AF=91=20=20=E5=9D=97=E4=B8=AD=20lifecycle.create/update=20=E7=9A=84?= =?UTF-8?q?=20params=20=E8=A1=A8=E8=BE=BE=E5=BC=8F=20-=20hap-compiler=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20lifecycle=20=E6=A8=A1=E5=9D=97(parseLifecy?= =?UTF-8?q?cle)=EF=BC=8C=E6=A0=A1=E9=AA=8C=20params=20=E4=B8=80=E7=BA=A7?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=20-=20hap-dsl-xvm=20=E6=96=B0=E5=A2=9E=20lif?= =?UTF-8?q?ecycle-loader=20=E5=8F=8A=E7=89=87=E6=AE=B5=E6=8E=A5=E5=85=A5?= =?UTF-8?q?=EF=BC=8C=E8=BD=BB=E5=8D=A1=E5=AF=BC=E5=87=BA=20lifecycle=20-?= =?UTF-8?q?=20hap-packager=20=E6=B3=A8=E5=86=8C=20lifecycle=20loader?= =?UTF-8?q?=EF=BC=8CpostHandleLiteCardRes=20=E5=A4=8D=E7=94=A8=20markParam?= =?UTF-8?q?s=20=E5=B0=86=20lifecycle=20params=20=E7=9A=84=20{{expr}}=20?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E4=B8=BA=20#/$=20JSON=20AST=EF=BC=8C?= =?UTF-8?q?=E9=9D=99=E6=80=81=E5=80=BC=E4=BF=9D=E6=8C=81=E5=8E=9F=E6=A0=B7?= =?UTF-8?q?=20=E9=9C=80=E6=B1=82=E4=BA=8C(P1)=EF=BC=9A=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E8=A1=A8=E8=BE=BE=E5=BC=8F=E7=A6=81=E6=AD=A2=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E8=80=97=E6=97=B6=20Feature=20-=20=E6=A0=A1=E9=AA=8C=20system.?= =?UTF-8?q?geolocation.getLocation=20/=20system.push.subscribe=20=E7=AD=89?= =?UTF-8?q?=E9=BB=91=E5=90=8D=E5=8D=95=20-=20=E5=91=BD=E4=B8=AD=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E8=A1=A8=E8=BE=BE=E5=BC=8F=E6=97=B6=E7=BC=96=E8=AF=91?= =?UTF-8?q?=E6=8A=A5=E9=94=99(=E5=90=AB=E8=A1=8C=E5=8F=B7=E4=B8=8E?= =?UTF-8?q?=E4=BF=AE=E6=94=B9=E5=BB=BA=E8=AE=AE)=EF=BC=8Cparams=20?= =?UTF-8?q?=E4=B8=AD=E6=94=BE=E8=A1=8C=20=E8=A1=A5=E5=85=85=20hap-packager?= =?UTF-8?q?=20=E5=8D=95=E6=B5=8B=E8=A6=86=E7=9B=96=E4=BB=A5=E4=B8=8A?= =?UTF-8?q?=E4=B8=A4=E4=B8=AA=E9=9C=80=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/hap-compiler/src/index.js | 24 ++- packages/hap-compiler/src/lifecycle/index.js | 49 ++++++ .../hap-compiler/src/lifecycle/validator.js | 36 ++++ packages/hap-compiler/src/template/index.js | 21 ++- .../hap-compiler/src/template/validator.js | 59 ++++++- .../hap-dsl-xvm/src/loaders/common/utils.js | 1 + .../src/loaders/lifecycle-loader.js | 18 ++ .../src/loaders/ux-fragment-utils.js | 45 +++++ packages/hap-dsl-xvm/src/loaders/ux-loader.js | 7 + packages/hap-packager/src/common/constant.js | 4 + .../src/post-handler/lite-card-post.js | 25 +++ .../func/__snapshots__/compiler.test.js.snap | 45 +++++ .../test/unit/func/compiler.test.js | 157 ++++++++++++++++++ 13 files changed, 486 insertions(+), 5 deletions(-) create mode 100644 packages/hap-compiler/src/lifecycle/index.js create mode 100644 packages/hap-compiler/src/lifecycle/validator.js create mode 100644 packages/hap-dsl-xvm/src/loaders/lifecycle-loader.js diff --git a/packages/hap-compiler/src/index.js b/packages/hap-compiler/src/index.js index 4e6ab2dc..d29a9916 100644 --- a/packages/hap-compiler/src/index.js +++ b/packages/hap-compiler/src/index.js @@ -7,11 +7,12 @@ import parse5 from 'parse5' import templater from './template' import actioner from './actions' +import lifecycler from './lifecycle' import styler from './style' import scripter from './script' import { serialize } from './utils' -export { scripter, styler, templater, actioner } +export { scripter, styler, templater, actioner, lifecycler } export * from './style' /** @@ -168,5 +169,24 @@ function parseActions(jsonObj) { return { parsed } } -export { parseFragmentsWithCache, parseTemplate, parseStyle, parseScript, parseActions, serialize } +/** + * 解析 lifecycle + * @param {Object} jsonObj - lifecycle对象 + * @returns {Object} + */ +function parseLifecycle(jsonObj) { + const { jsonLifecycle } = lifecycler.parse(jsonObj) + const parsed = JSON.stringify(jsonLifecycle) + return { parsed } +} + +export { + parseFragmentsWithCache, + parseTemplate, + parseStyle, + parseScript, + parseActions, + parseLifecycle, + serialize +} export { ENTRY_TYPE, FRAG_TYPE, isEmptyObject } from './utils' diff --git a/packages/hap-compiler/src/lifecycle/index.js b/packages/hap-compiler/src/lifecycle/index.js new file mode 100644 index 00000000..90f975ec --- /dev/null +++ b/packages/hap-compiler/src/lifecycle/index.js @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024-present, the hapjs-platform Project Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { checkParams } from './validator' + +// lifecycle 支持的钩子 +const LIFECYCLE_HOOKS = ['create', 'update'] + +function parse(lifecycleObj) { + /** + * "lifecycleObj": { + "create": { + "params": { + "lat": "{{ system.geolocation.getLocation({coordType:'gcj02'}).latitude }}", + "deviceType": "{{ system.device.DEVICE_TYPE }}" + } + }, + "update": { + "params": { + "deviceType": "{{ system.device.DEVICE_TYPE }}" + } + } + } + */ + if (lifecycleObj && Object.prototype.toString.call(lifecycleObj) !== '[object Object]') { + throw new Error(` lifecycle 必须为 Object 对象`) + } + + // 校验 create/update 钩子的 params 合法性 + LIFECYCLE_HOOKS.forEach((hook) => { + const hookObj = lifecycleObj[hook] + if (hookObj) { + if (Object.prototype.toString.call(hookObj) !== '[object Object]') { + throw new Error(` lifecycle.${hook} 必须为 Object 对象`) + } + checkParams(hookObj.params, 1) + } + }) + + return { + jsonLifecycle: lifecycleObj + } +} + +export default { + parse +} diff --git a/packages/hap-compiler/src/lifecycle/validator.js b/packages/hap-compiler/src/lifecycle/validator.js new file mode 100644 index 00000000..1163dafc --- /dev/null +++ b/packages/hap-compiler/src/lifecycle/validator.js @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024-present, the hapjs-platform Project Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import exp from '../template/exp' + +function isReserved(str) { + // $开头 + const c = (str + '').charCodeAt(0) + return c === 0x24 +} + +/** + * 校验 lifecycle 中 create/update 的 params 参数 + * 规则与 actions params 一致:只支持一级结构中绑定变量,参数名不能以 $ 开头 + * @param {Object} paramsObj - params 对象 + * @param {number} dep - 当前嵌套深度 + */ +function checkParams(paramsObj, dep) { + if (!paramsObj || Object.prototype.toString.call(paramsObj) !== '[object Object]') return + + Object.keys(paramsObj).forEach((key) => { + if (isReserved(key)) { + throw new Error(` lifecycle 中 params 参数名不能以 “$” 开头`) + } + + if (exp.isExpr(paramsObj[key]) && dep > 1) { + throw new Error(` lifecycle 中 params 参数值只支持一级结构中绑定变量`) + } + + checkParams(paramsObj[key], dep + 1) + }) +} + +export { checkParams } diff --git a/packages/hap-compiler/src/template/index.js b/packages/hap-compiler/src/template/index.js index 75ad00a8..f161e557 100644 --- a/packages/hap-compiler/src/template/index.js +++ b/packages/hap-compiler/src/template/index.js @@ -72,6 +72,9 @@ function traverse(node, output, previousNode, conditionList, options) { } } + // 编译期护栏:禁止在模板表达式中调用耗时 Feature + validator.checkForbiddenFeature(value, locationInfo, options) + switch (name) { case 'id': // 保留checkId为兼容原有:新打的RPK包兼容原来的APK平台 @@ -169,6 +172,12 @@ function traverse(node, output, previousNode, conditionList, options) { if (child.nodeName.match(/^#/)) { // 处理#text节点内容 if (child.nodeName === '#text' && child.value.trim()) { + // 编译期护栏:禁止在模板文本表达式中调用耗时 Feature + validator.checkForbiddenFeature( + child.value, + node.__location ? { line: node.__location.line, column: node.__location.col } : null, + options + ) // 兄弟节点不为自闭合标签且非文本标签非原子组件 if (!preNode || !validator.isSupportedSelfClosing(preNode.nodeName)) { if (validator.isNotTextContentAtomic(node.tagName)) { @@ -420,7 +429,17 @@ function parse(source, options) { try { traverse(current, output, null, null, options) } catch (err) { - if (err.isExpressionError) { + if (err.isForbiddenFeatureError) { + output.log.push({ + line: err.line, + column: err.column, + reason: + `ERROR: 耗时 Feature "${err.feature}" 不允许在模板表达式中使用。\n` + + ` 原因:模板表达式在 UI 线程同步执行,耗时调用会阻塞渲染。\n` + + ` 建议:将该调用移到 lifecycle.create.params 或 action params 中。\n` + + ` 表达式:${err.expression}\n at ${options.filePath}` + }) + } else if (err.isExpressionError) { output.log.push({ reason: `ERROR: 表达式解析失败 ${err.message}\n\n> ${err.expression}\n\nat ${options.filePath}` }) diff --git a/packages/hap-compiler/src/template/validator.js b/packages/hap-compiler/src/template/validator.js index 0f04b77c..98e40965 100644 --- a/packages/hap-compiler/src/template/validator.js +++ b/packages/hap-compiler/src/template/validator.js @@ -217,7 +217,15 @@ const tagNatives = { }, web: { atomic: true, - events: ['pagestart', 'pagefinish', 'titlereceive', 'error', 'message', 'progress', 'intercepturl'], + events: [ + 'pagestart', + 'pagefinish', + 'titlereceive', + 'error', + 'message', + 'progress', + 'intercepturl' + ], attrs: { src: {}, trustedurl: {}, @@ -233,7 +241,7 @@ const tagNatives = { supportzoom: { enum: ['true', 'false'] }, - intercepturl:{} + intercepturl: {} } }, list: { @@ -1944,6 +1952,52 @@ function checkCustomDirective(name, value, output, node, options) { } } +// 耗时 Feature 黑名单:这些 Feature 在 UI 线程同步执行会阻塞渲染, +// 禁止出现在模板表达式中(允许出现在 lifecycle params / action params 中) +const FORBIDDEN_TEMPLATE_FEATURES = ['system.geolocation.getLocation', 'system.push.subscribe'] + +// 为每个黑名单 Feature 构建匹配「方法调用」的正则(允许任意参数、段间空白) +const FORBIDDEN_FEATURE_MATCHERS = FORBIDDEN_TEMPLATE_FEATURES.map((feature) => { + const escaped = feature + .split('.') + .map((seg) => seg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + .join('\\s*\\.\\s*') + return { + feature, + // 前置一个非标识符/非点的边界,避免误匹配 foo.system.geolocation... 之类的其它路径 + re: new RegExp('(?:^|[^\\w$.])' + escaped + '\\s*\\(') + } +}) + +/** + * 校验模板表达式中是否使用了耗时 Feature(编译期护栏) + * 命中黑名单时抛出错误,由 template/index.js 的 parse() 汇入 output.log 报错。 + * 仅对轻卡(options.lite)生效;lifecycle/action params 不经过模板校验,因此天然放行。 + * @param {string} value - 属性值 / 文本内容 + * @param {object} locationInfo - 位置信息 {line, column} + * @param {object} options - 编译选项 + */ +function checkForbiddenFeature(value, locationInfo, options) { + if (!options || !options.lite) return + if (!value || typeof value !== 'string') return + if (!exp.isExpr(value)) return + + for (let i = 0; i < FORBIDDEN_FEATURE_MATCHERS.length; i++) { + const { feature, re } = FORBIDDEN_FEATURE_MATCHERS[i] + if (re.test(value)) { + const err = new Error(`耗时 Feature "${feature}" 不允许在模板表达式中使用`) + err.isForbiddenFeatureError = true + err.feature = feature + err.expression = value.trim() + if (locationInfo) { + err.line = locationInfo.line + err.column = locationInfo.column + } + throw err + } + } +} + /** * @param {string} name * @param {string} value @@ -2150,6 +2204,7 @@ export default { checkEvent, checkCustomDirective, checkAttr, + checkForbiddenFeature, checkBuild, checkModel, isReservedTag, diff --git a/packages/hap-dsl-xvm/src/loaders/common/utils.js b/packages/hap-dsl-xvm/src/loaders/common/utils.js index 08b5dd8e..d64a0a27 100644 --- a/packages/hap-dsl-xvm/src/loaders/common/utils.js +++ b/packages/hap-dsl-xvm/src/loaders/common/utils.js @@ -248,6 +248,7 @@ export const FRAG_TYPE = { SCRIPT: 'script', DATA: 'data', ACTIONS: 'actions', + LIFECYCLE: 'lifecycle', PROPS: 'props', // honor frag DATAPP: 'app', diff --git a/packages/hap-dsl-xvm/src/loaders/lifecycle-loader.js b/packages/hap-dsl-xvm/src/loaders/lifecycle-loader.js new file mode 100644 index 00000000..54db63ab --- /dev/null +++ b/packages/hap-dsl-xvm/src/loaders/lifecycle-loader.js @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2024-present, the hapjs-platform Project Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { parseLifecycle } from '@hap-toolkit/compiler' + +export default function lifecycleLoader(source) { + let lifecycleStr = '' + try { + const obj = JSON.parse(source) + const jsonObj = obj.lifecycle || {} + const { parsed } = parseLifecycle(jsonObj) + lifecycleStr = parsed + } catch (e) { + throw new Error(`${this.resourcePath} 中的 解析失败,请检查是否为标准的 JSON 格式\n${e}`) + } + return `module.exports = ${lifecycleStr}` +} diff --git a/packages/hap-dsl-xvm/src/loaders/ux-fragment-utils.js b/packages/hap-dsl-xvm/src/loaders/ux-fragment-utils.js index 403eb2ec..382bdfb6 100644 --- a/packages/hap-dsl-xvm/src/loaders/ux-fragment-utils.js +++ b/packages/hap-dsl-xvm/src/loaders/ux-fragment-utils.js @@ -30,6 +30,7 @@ const defaultLoaders = { component: resolveSync('./ux-loader.js'), data: resolveSync('./data-loader.js'), action: resolveSync('./action-loader.js'), + lifecycle: resolveSync('./lifecycle-loader.js'), props: resolveSync('./props-loader.js'), fragment: resolveSync('./fragment-loader.js'), template: resolveSync('./template-loader.js'), @@ -123,6 +124,22 @@ function makeLoaderString(type, config, newJSCard, uxType) { return stringifyLoaders(loaders) } + if (type === FRAG_TYPE.LIFECYCLE) { + loaders = [ + { + name: defaultLoaders.lifecycle + }, + { + name: defaultLoaders.fragment, + query: { + index: 0, + type: FRAG_TYPE.DATA + } + } + ] + return stringifyLoaders(loaders) + } + // honor loader if (type === FRAG_TYPE.DATAPP) { loaders = [ @@ -554,6 +571,33 @@ function processActionFrag($loader, datas, uxType) { return code } +/** + * 处理轻卡片段中lifecycle + * @param $loader + * @param datas + * @param uxType + * @returns {string} + */ +function processLifecycleFrag($loader, datas, uxType) { + let code = 'null' + if (datas.length) { + // 有且仅有一个节点 + const data = datas[0] + // 文件绝对路径 + let src = $loader.resourcePath + const fragAttrsSrc = data.attrs.src + if (fragAttrsSrc) { + src = fragAttrsSrc + } + code = makeRequireString( + $loader, + makeLoaderString(FRAG_TYPE.LIFECYCLE, {}, true, uxType), + `${src}?index=0&lite=1` + ) + } + return code +} + /** * 处理轻卡自定义组件片段中props * @param $loader @@ -682,6 +726,7 @@ export { processScriptFrag, processDataFrag, processActionFrag, + processLifecycleFrag, processPropsFrag, parseImportList, // honor process frag diff --git a/packages/hap-dsl-xvm/src/loaders/ux-loader.js b/packages/hap-dsl-xvm/src/loaders/ux-loader.js index 6125b7bb..54028e54 100644 --- a/packages/hap-dsl-xvm/src/loaders/ux-loader.js +++ b/packages/hap-dsl-xvm/src/loaders/ux-loader.js @@ -15,6 +15,7 @@ import { processScriptFrag, processDataFrag, processActionFrag, + processLifecycleFrag, processPropsFrag, parseImportList, // honor process frag @@ -94,6 +95,12 @@ function assemble($loader, frags, name, queryOptions) { uxType, FRAG_TYPE.ACTIONS )}\n` + content += ` $app_module$.exports.lifecycle = ${processLifecycleFrag( + $loader, + frags.data, + uxType, + FRAG_TYPE.LIFECYCLE + )}\n` content += ` $app_module$.exports.props = ${processPropsFrag( $loader, frags.data, diff --git a/packages/hap-packager/src/common/constant.js b/packages/hap-packager/src/common/constant.js index 01e40806..ae233b4c 100644 --- a/packages/hap-packager/src/common/constant.js +++ b/packages/hap-packager/src/common/constant.js @@ -71,6 +71,10 @@ const LOADER_INFO_LIST = [ path: '/action-loader.js', type: 'actions' }, + { + path: '/lifecycle-loader.js', + type: 'lifecycle' + }, { path: '/props-loader.js', type: 'props' diff --git a/packages/hap-packager/src/post-handler/lite-card-post.js b/packages/hap-packager/src/post-handler/lite-card-post.js index 959da6b1..144460e9 100644 --- a/packages/hap-packager/src/post-handler/lite-card-post.js +++ b/packages/hap-packager/src/post-handler/lite-card-post.js @@ -12,6 +12,9 @@ const TYPE_IMPORT = 'import' // 需要进行后处理的模块key const TEMPLATE_KEY = 'template' const ACTIONS_KEY = 'actions' +const LIFECYCLE_KEY = 'lifecycle' +// lifecycle 支持的钩子 +const LIFECYCLE_HOOKS = ['create', 'update'] // 节点标记,同一节点可能同时符合多个kind定义,按priority高的进行标记 const ENUM_KIND_TYPE = { @@ -112,6 +115,19 @@ function markParams(actions) { }) } +// 处理 lifecycle 中的字段,编译 create/update 的 params 表达式 +// 与 actions params 使用相同的编译规则(复用 markParams) +function postHandleLifecycle(lifecycle) { + if (!isObject(lifecycle)) return + + LIFECYCLE_HOOKS.forEach((hook) => { + const hookObj = lifecycle[hook] + if (isObject(hookObj)) { + markParams(hookObj) + } + }) +} + // 处理 template 中的字段,增加标记和处理表达式 function postHandleCompTemplate(template, liteCardRes, compName) { if (!isObject(template)) return @@ -385,5 +401,14 @@ export function postHandleLiteCardRes(liteCardRes) { } } + // lifecycle + for (let i = 0; i < uxList.length; i++) { + const compName = uxList[i] + const lifecycle = liteCardRes[compName][LIFECYCLE_KEY] + if (lifecycle) { + postHandleLifecycle(lifecycle) + } + } + return liteCardRes } diff --git a/packages/hap-packager/test/unit/func/__snapshots__/compiler.test.js.snap b/packages/hap-packager/test/unit/func/__snapshots__/compiler.test.js.snap index 3fa58f94..7aad18ac 100644 --- a/packages/hap-packager/test/unit/func/__snapshots__/compiler.test.js.snap +++ b/packages/hap-packager/test/unit/func/__snapshots__/compiler.test.js.snap @@ -28,3 +28,48 @@ exports[`compile functions replaceModuleImport 4`] = ` const system = $app_require$(\\"@app-module/system\\") " `; + +exports[`轻卡 lifecycle params 编译 postHandleLiteCardRes 将 lifecycle params 中的 {{ expr }} 编译为 #/$ AST 1`] = ` +Object { + "#deviceType": Array [ + ".", + Array [ + ".", + Array [ + "$", + "system", + ], + "device", + ], + "DEVICE_TYPE", + ], + "#lat": Array [ + ".", + Array [ + "()", + Array [ + ".", + Array [ + ".", + Array [ + "$", + "system", + ], + "geolocation", + ], + "getLocation", + ], + Array [ + "{}", + Object { + "coordType": "gcj02", + }, + ], + ], + "latitude", + ], + "$deviceType": "system.device.DEVICE_TYPE", + "$lat": "{{ system.geolocation.getLocation({coordType:'gcj02'}).latitude }}", + "staticVal": "plain-string", +} +`; diff --git a/packages/hap-packager/test/unit/func/compiler.test.js b/packages/hap-packager/test/unit/func/compiler.test.js index 9ecf6f87..b93cd1b5 100644 --- a/packages/hap-packager/test/unit/func/compiler.test.js +++ b/packages/hap-packager/test/unit/func/compiler.test.js @@ -4,6 +4,8 @@ */ const { replaceModuleImport } = require('@hap-toolkit/compiler').scripter +const { parseLifecycle, parseTemplate } = require('@hap-toolkit/compiler') +const { postHandleLiteCardRes } = require('../../../lib/post-handler/lite-card-post') describe('compile functions', () => { it('replaceModuleImport', () => { @@ -32,3 +34,158 @@ describe('compile functions', () => { expect(requireExp2).toMatchSnapshot() }) }) + +/** + * 轻卡需求一:lifecycle params 表达式编译 + * 块中的 lifecycle.create/update.params 中的 {{ expr }} 需要编译为 JSON AST, + * 编译规则与 actions params 一致(#key 为 AST,$key 为原文,纯静态值保持原样)。 + */ +describe('轻卡 lifecycle params 编译', () => { + it('parseLifecycle 校验通过并返回合法 JSON', () => { + const { parsed } = parseLifecycle({ + create: { + params: { + lat: "{{ system.geolocation.getLocation({coordType:'gcj02'}).latitude }}", + deviceType: '{{ system.device.DEVICE_TYPE }}' + } + }, + update: { + params: { + deviceType: '{{ system.device.DEVICE_TYPE }}' + } + } + }) + const obj = JSON.parse(parsed) + expect(obj.create.params.lat).toBeDefined() + expect(obj.update.params.deviceType).toBeDefined() + }) + + it('parseLifecycle 拒绝以 $ 开头的 params 参数名', () => { + expect(() => { + parseLifecycle({ create: { params: { $bad: '{{ a }}' } } }) + }).toThrow() + }) + + it('parseLifecycle 拒绝多级结构中绑定变量', () => { + expect(() => { + parseLifecycle({ create: { params: { obj: { nested: '{{ a }}' } } } }) + }).toThrow() + }) + + it('postHandleLiteCardRes 将 lifecycle params 中的 {{ expr }} 编译为 #/$ AST', () => { + const liteCardRes = { + '#entry': { + template: { type: 'div', children: [] }, + data: { title: 'x' }, + lifecycle: { + create: { + params: { + lat: "{{ system.geolocation.getLocation({coordType:'gcj02'}).latitude }}", + deviceType: '{{ system.device.DEVICE_TYPE }}', + staticVal: 'plain-string' + } + }, + update: { + params: { + deviceType: '{{ system.device.DEVICE_TYPE }}' + } + } + } + } + } + const res = postHandleLiteCardRes(liteCardRes) + const createParams = res['#entry'].lifecycle.create.params + + // 表达式被编译为 #key(AST) + $key(原文) 对 + expect(Array.isArray(createParams['#lat'])).toBe(true) + expect(createParams['#lat'][0]).toBe('.') + expect(createParams).toHaveProperty('$lat') + expect(Array.isArray(createParams['#deviceType'])).toBe(true) + // 纯静态值保持原样,不加前缀 + expect(createParams.staticVal).toBe('plain-string') + expect(createParams['#staticVal']).toBeUndefined() + // update.params 同样被编译 + expect(Array.isArray(res['#entry'].lifecycle.update.params['#deviceType'])).toBe(true) + + // 记录完整编译产物,锁定 AST 格式 + expect(createParams).toMatchSnapshot() + }) + + it('lifecycle 编译不影响 actions / data 的既有行为', () => { + const liteCardRes = { + '#entry': { + template: { type: 'div', children: [] }, + data: { title: 'x' }, + actions: { + onTap: { type: 'message', params: { d: '{{ system.device.DEVICE_TYPE }}' } } + }, + lifecycle: { create: { params: { a: '{{ system.device.DEVICE_TYPE }}' } } } + } + } + const res = postHandleLiteCardRes(liteCardRes) + expect(Array.isArray(res['#entry'].actions.onTap.params['#d'])).toBe(true) + expect(res['#entry'].data.title).toBe('x') + }) +}) + +/** + * 轻卡需求二:模板中禁止耗时 Feature 调用(编译期校验) + *