diff --git a/examples/sample/package.json b/examples/sample/package.json index b79b7ea8..4c7f5aba 100644 --- a/examples/sample/package.json +++ b/examples/sample/package.json @@ -1,6 +1,6 @@ { "name": "sample", - "version": "2.0.9-beta.1", + "version": "2.0.9-beta.3", "subversion": { "toolkit": "0.3.1" }, @@ -23,7 +23,7 @@ "@babel/runtime": "^7.9.6", "catching": "^1.0.2", "fkill": "^5.3.0", - "hap-toolkit": "2.0.9-beta.1", + "hap-toolkit": "2.0.9-beta.3", "node-fetch": "^2.6.0", "npm-run-all": "^4.1.5", "qrcode-js": "0.0.2" diff --git a/lerna.json b/lerna.json index ee4ccf8f..e9c4c19a 100644 --- a/lerna.json +++ b/lerna.json @@ -4,5 +4,5 @@ "packages/*", "test/build/projects/*" ], - "version": "2.0.9-beta.1" + "version": "2.0.9-beta.3" } diff --git a/packages/hap-compiler/package.json b/packages/hap-compiler/package.json index da6041ca..cb700947 100644 --- a/packages/hap-compiler/package.json +++ b/packages/hap-compiler/package.json @@ -1,6 +1,6 @@ { "name": "@hap-toolkit/compiler", - "version": "2.0.9-beta.1", + "version": "2.0.9-beta.3", "description": "compiler of hap-toolkit", "engines": { "node": ">=14.0.0" @@ -22,7 +22,7 @@ "lib/**/*.js" ], "dependencies": { - "@hap-toolkit/shared-utils": "2.0.9-beta.1", + "@hap-toolkit/shared-utils": "2.0.9-beta.3", "@jayfate/path": "^0.0.13", "css": "^2.2.4", "css-what": "^2.1.3", 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..286d8e5d --- /dev/null +++ b/packages/hap-compiler/src/lifecycle/index.js @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2024-present, the hapjs-platform Project Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { checkParams } from './validator' + +/** + * 解析并校验单个生命周期钩子(create 或 update)对象 + * @param {Object} hookObj - 形如 { params: { ... } } + * @returns {Object} + */ +function parse(hookObj) { + /** + * "hookObj": { + "params": { + "lat": "{{ system.geolocation.getLocation({coordType:'gcj02'}).latitude }}", + "deviceType": "{{ system.device.DEVICE_TYPE }}" + } + } + */ + if (hookObj && Object.prototype.toString.call(hookObj) !== '[object Object]') { + throw new Error(` create/update 必须为 Object 对象`) + } + + // 校验 params 合法性(规则与 actions params 一致) + checkParams(hookObj.params, 1) + + return { + jsonLifecycle: hookObj + } +} + +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..5c6583e3 --- /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 +} + +/** + * 校验 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(` create/update 中 params 参数名不能以 “$” 开头`) + } + + if (exp.isExpr(paramsObj[key]) && dep > 1) { + throw new Error(` create/update 中 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..7e07ab57 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,19 @@ 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, + // 致命错误:需要中断构建(由 template-loader 上报为 webpack 编译错误) + fatal: true, + reason: + `ERROR: 耗时 Feature "${err.feature}" 不允许在模板表达式中使用。\n` + + ` 原因:模板表达式在 UI 线程同步执行,耗时调用会阻塞渲染。\n` + + ` 建议:将该调用移到 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..9c71c4c2 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', 'service.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-debugger/package.json b/packages/hap-debugger/package.json index 29304dab..cef583df 100644 --- a/packages/hap-debugger/package.json +++ b/packages/hap-debugger/package.json @@ -1,6 +1,6 @@ { "name": "@hap-toolkit/debugger", - "version": "2.0.9-beta.1", + "version": "2.0.9-beta.3", "description": "@hap-toolkit/debugger", "engines": { "node": ">=14.0.0" @@ -29,7 +29,7 @@ ], "dependencies": { "@hap-toolkit/chrome-devtools-frontend": "^1.1.2", - "@hap-toolkit/shared-utils": "2.0.9-beta.1", + "@hap-toolkit/shared-utils": "2.0.9-beta.3", "@jayfate/path": "^0.0.13", "@sentry/node": "^5.26.0", "adb-commander": "^0.1.9", diff --git a/packages/hap-dev-utils/package.json b/packages/hap-dev-utils/package.json index 57ff9a19..2c26b557 100644 --- a/packages/hap-dev-utils/package.json +++ b/packages/hap-dev-utils/package.json @@ -1,6 +1,6 @@ { "name": "hap-dev-utils", - "version": "2.0.9-beta.1", + "version": "2.0.9-beta.3", "private": true, "description": "utils of development and testing", "engines": { diff --git a/packages/hap-dsl-xvm/package.json b/packages/hap-dsl-xvm/package.json index 7c9f62b8..b713a93d 100644 --- a/packages/hap-dsl-xvm/package.json +++ b/packages/hap-dsl-xvm/package.json @@ -1,6 +1,6 @@ { "name": "@hap-toolkit/dsl-xvm", - "version": "2.0.9-beta.1", + "version": "2.0.9-beta.3", "description": "webpack {loader,plugin} for supporting xvm in quickapp", "engines": { "node": ">=14.0.0" @@ -36,9 +36,9 @@ "!**/.DS_Store" ], "dependencies": { - "@hap-toolkit/compiler": "2.0.9-beta.1", - "@hap-toolkit/packager": "2.0.9-beta.1", - "@hap-toolkit/shared-utils": "2.0.9-beta.1", + "@hap-toolkit/compiler": "2.0.9-beta.3", + "@hap-toolkit/packager": "2.0.9-beta.3", + "@hap-toolkit/shared-utils": "2.0.9-beta.3", "@jayfate/path": "^0.0.13", "babel-loader": "^8.1.0", "css-loader": "^5.0.1", diff --git a/packages/hap-dsl-xvm/src/loaders/common/utils.js b/packages/hap-dsl-xvm/src/loaders/common/utils.js index 08b5dd8e..91c5c19d 100644 --- a/packages/hap-dsl-xvm/src/loaders/common/utils.js +++ b/packages/hap-dsl-xvm/src/loaders/common/utils.js @@ -248,6 +248,8 @@ export const FRAG_TYPE = { SCRIPT: 'script', DATA: 'data', ACTIONS: 'actions', + CREATE: 'create', + UPDATE: 'update', PROPS: 'props', // honor frag DATAPP: 'app', diff --git a/packages/hap-dsl-xvm/src/loaders/create-loader.js b/packages/hap-dsl-xvm/src/loaders/create-loader.js new file mode 100644 index 00000000..f92cdf37 --- /dev/null +++ b/packages/hap-dsl-xvm/src/loaders/create-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 createLoader(source) { + let createStr = '' + try { + const obj = JSON.parse(source) + const jsonObj = obj.create || {} + const { parsed } = parseLifecycle(jsonObj) + createStr = parsed + } catch (e) { + throw new Error(`${this.resourcePath} 中的 解析失败,请检查是否为标准的 JSON 格式\n${e}`) + } + return `module.exports = ${createStr}` +} diff --git a/packages/hap-dsl-xvm/src/loaders/template-loader.js b/packages/hap-dsl-xvm/src/loaders/template-loader.js index 964c8e6e..a2f672a7 100644 --- a/packages/hap-dsl-xvm/src/loaders/template-loader.js +++ b/packages/hap-dsl-xvm/src/loaders/template-loader.js @@ -20,6 +20,13 @@ export default function templateLoader(source) { if (log && log.length) { logWarn(this, log) + // 模板编译中的致命错误需要上报为 webpack 编译错误,以中断构建并使进程非零退出 + log.forEach((item) => { + if (item.fatal) { + const locationInfo = item.line && item.column ? ` @${item.line}:${item.column}` : '' + this.emitError(new Error(`${this.resourcePath}${locationInfo} ${item.reason}`)) + } + }) } depFiles.forEach((file) => { let fileName = file diff --git a/packages/hap-dsl-xvm/src/loaders/update-loader.js b/packages/hap-dsl-xvm/src/loaders/update-loader.js new file mode 100644 index 00000000..f51429e4 --- /dev/null +++ b/packages/hap-dsl-xvm/src/loaders/update-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 updateLoader(source) { + let updateStr = '' + try { + const obj = JSON.parse(source) + const jsonObj = obj.update || {} + const { parsed } = parseLifecycle(jsonObj) + updateStr = parsed + } catch (e) { + throw new Error(`${this.resourcePath} 中的 解析失败,请检查是否为标准的 JSON 格式\n${e}`) + } + return `module.exports = ${updateStr}` +} 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..4ce20245 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,8 @@ const defaultLoaders = { component: resolveSync('./ux-loader.js'), data: resolveSync('./data-loader.js'), action: resolveSync('./action-loader.js'), + create: resolveSync('./create-loader.js'), + update: resolveSync('./update-loader.js'), props: resolveSync('./props-loader.js'), fragment: resolveSync('./fragment-loader.js'), template: resolveSync('./template-loader.js'), @@ -123,6 +125,38 @@ function makeLoaderString(type, config, newJSCard, uxType) { return stringifyLoaders(loaders) } + if (type === FRAG_TYPE.CREATE) { + loaders = [ + { + name: defaultLoaders.create + }, + { + name: defaultLoaders.fragment, + query: { + index: 0, + type: FRAG_TYPE.DATA + } + } + ] + return stringifyLoaders(loaders) + } + + if (type === FRAG_TYPE.UPDATE) { + loaders = [ + { + name: defaultLoaders.update + }, + { + name: defaultLoaders.fragment, + query: { + index: 0, + type: FRAG_TYPE.DATA + } + } + ] + return stringifyLoaders(loaders) + } + // honor loader if (type === FRAG_TYPE.DATAPP) { loaders = [ @@ -554,6 +588,48 @@ function processActionFrag($loader, datas, uxType) { return code } +/** + * 处理轻卡片段中生命周期钩子(create/update) + * @param $loader + * @param datas + * @param uxType + * @param {string} fragType - FRAG_TYPE.CREATE 或 FRAG_TYPE.UPDATE + * @returns {string} + */ +function processLifecycleFrag($loader, datas, uxType, fragType) { + 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(fragType, {}, true, uxType), + `${src}?index=0&lite=1` + ) + } + return code +} + +/** + * 处理轻卡片段中 create 钩子 + */ +function processCreateFrag($loader, datas, uxType) { + return processLifecycleFrag($loader, datas, uxType, FRAG_TYPE.CREATE) +} + +/** + * 处理轻卡片段中 update 钩子 + */ +function processUpdateFrag($loader, datas, uxType) { + return processLifecycleFrag($loader, datas, uxType, FRAG_TYPE.UPDATE) +} + /** * 处理轻卡自定义组件片段中props * @param $loader @@ -682,6 +758,8 @@ export { processScriptFrag, processDataFrag, processActionFrag, + processCreateFrag, + processUpdateFrag, 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..a7361c3e 100644 --- a/packages/hap-dsl-xvm/src/loaders/ux-loader.js +++ b/packages/hap-dsl-xvm/src/loaders/ux-loader.js @@ -15,6 +15,8 @@ import { processScriptFrag, processDataFrag, processActionFrag, + processCreateFrag, + processUpdateFrag, processPropsFrag, parseImportList, // honor process frag @@ -94,6 +96,18 @@ function assemble($loader, frags, name, queryOptions) { uxType, FRAG_TYPE.ACTIONS )}\n` + content += ` $app_module$.exports.create = ${processCreateFrag( + $loader, + frags.data, + uxType, + FRAG_TYPE.CREATE + )}\n` + content += ` $app_module$.exports.update = ${processUpdateFrag( + $loader, + frags.data, + uxType, + FRAG_TYPE.UPDATE + )}\n` content += ` $app_module$.exports.props = ${processPropsFrag( $loader, frags.data, diff --git a/packages/hap-packager/package.json b/packages/hap-packager/package.json index 0e03b58c..6b0338c7 100644 --- a/packages/hap-packager/package.json +++ b/packages/hap-packager/package.json @@ -1,6 +1,6 @@ { "name": "@hap-toolkit/packager", - "version": "2.0.9-beta.1", + "version": "2.0.9-beta.3", "description": "@hap-toolkit/packager", "engines": { "node": ">=14.0.0" @@ -44,8 +44,8 @@ "@babel/traverse": "^7.9.6", "@babel/types": "^7.9.6", "@hap-toolkit/aaptjs": "^2.0.0", - "@hap-toolkit/compiler": "2.0.9-beta.1", - "@hap-toolkit/shared-utils": "2.0.9-beta.1", + "@hap-toolkit/compiler": "2.0.9-beta.3", + "@hap-toolkit/shared-utils": "2.0.9-beta.3", "@jayfate/path": "^0.0.13", "babel-loader": "^8.1.0", "flat": "^5.0.2", diff --git a/packages/hap-packager/src/common/constant.js b/packages/hap-packager/src/common/constant.js index 01e40806..029e3e35 100644 --- a/packages/hap-packager/src/common/constant.js +++ b/packages/hap-packager/src/common/constant.js @@ -71,6 +71,14 @@ const LOADER_INFO_LIST = [ path: '/action-loader.js', type: 'actions' }, + { + path: '/create-loader.js', + type: 'create' + }, + { + path: '/update-loader.js', + type: 'update' + }, { path: '/props-loader.js', type: 'props' diff --git a/packages/hap-packager/src/plugins/zip-plugin.js b/packages/hap-packager/src/plugins/zip-plugin.js index 681ae15f..a2f3dd3e 100644 --- a/packages/hap-packager/src/plugins/zip-plugin.js +++ b/packages/hap-packager/src/plugins/zip-plugin.js @@ -291,6 +291,13 @@ ZipPlugin.prototype.apply = function (compiler) { const options = this.options compiler.hooks.done.tapAsync('ZipPlugin', async (stats, callback) => { + // 编译存在错误时,跳过打包,不生成 rpk/rpks,避免产出有问题的应用包 + if (stats.compilation.errors && stats.compilation.errors.length) { + colorconsole.error('### App Loader ### 编译存在错误,已停止生成应用包') + callback() + return + } + let subpackageOptions = [] if (!options.disableSubpackages && options.subpackages && options.subpackages.length > 0) { subpackageOptions = [...options.subpackages] 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..ae562dd7 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,8 @@ const TYPE_IMPORT = 'import' // 需要进行后处理的模块key const TEMPLATE_KEY = 'template' const ACTIONS_KEY = 'actions' +// 生命周期钩子(编译产物中为 #entry 顶层字段) +const LIFECYCLE_HOOK_KEYS = ['create', 'update'] // 节点标记,同一节点可能同时符合多个kind定义,按priority高的进行标记 const ENUM_KIND_TYPE = { @@ -385,5 +387,17 @@ export function postHandleLiteCardRes(liteCardRes) { } } + // 生命周期钩子 create / update(#entry 顶层字段),编译其 params 表达式 + // 与 actions params 使用相同的编译规则(复用 markParams) + for (let i = 0; i < uxList.length; i++) { + const compName = uxList[i] + LIFECYCLE_HOOK_KEYS.forEach((hookKey) => { + const hookObj = liteCardRes[compName][hookKey] + if (isObject(hookObj)) { + markParams(hookObj) + } + }) + } + 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..3db140dc 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[`轻卡 create/update params 编译 postHandleLiteCardRes 将 create/update 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..d3abeff9 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,152 @@ describe('compile functions', () => { expect(requireExp2).toMatchSnapshot() }) }) + +/** + * 轻卡需求一:create/update params 表达式编译 + * 块中的顶层 create/update 的 params 中的 {{ expr }} 需要编译为 JSON AST, + * 编译规则与 actions params 一致(#key 为 AST,$key 为原文,纯静态值保持原样)。 + * 编译产出中 create/update 为 #entry 顶层字段(不再包裹 lifecycle 一层)。 + */ +describe('轻卡 create/update params 编译', () => { + it('parseLifecycle 校验单个钩子并返回合法 JSON', () => { + const { parsed } = parseLifecycle({ + params: { + lat: "{{ system.geolocation.getLocation({coordType:'gcj02'}).latitude }}", + deviceType: '{{ system.device.DEVICE_TYPE }}' + } + }) + const obj = JSON.parse(parsed) + expect(obj.params.lat).toBeDefined() + expect(obj.params.deviceType).toBeDefined() + }) + + it('parseLifecycle 拒绝以 $ 开头的 params 参数名', () => { + expect(() => { + parseLifecycle({ params: { $bad: '{{ a }}' } }) + }).toThrow() + }) + + it('parseLifecycle 拒绝多级结构中绑定变量', () => { + expect(() => { + parseLifecycle({ params: { obj: { nested: '{{ a }}' } } }) + }).toThrow() + }) + + it('postHandleLiteCardRes 将 create/update params 中的 {{ expr }} 编译为 #/$ AST', () => { + const liteCardRes = { + '#entry': { + template: { type: 'div', children: [] }, + data: { title: 'x' }, + 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'].create.params + + // create/update 是 #entry 顶层字段,没有 lifecycle 包裹 + expect(res['#entry'].lifecycle).toBeUndefined() + // 表达式被编译为 #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'].update.params['#deviceType'])).toBe(true) + + // 记录完整编译产物,锁定 AST 格式 + expect(createParams).toMatchSnapshot() + }) + + it('create/update 编译不影响 actions / data 的既有行为', () => { + const liteCardRes = { + '#entry': { + template: { type: 'div', children: [] }, + data: { title: 'x' }, + actions: { + onTap: { type: 'message', params: { d: '{{ system.device.DEVICE_TYPE }}' } } + }, + 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 调用(编译期校验) + *