Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/sample/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "sample",
"version": "2.0.9-beta.1",
"version": "2.0.9-beta.3",
"subversion": {
"toolkit": "0.3.1"
},
Expand All @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"packages/*",
"test/build/projects/*"
],
"version": "2.0.9-beta.1"
"version": "2.0.9-beta.3"
}
4 changes: 2 additions & 2 deletions packages/hap-compiler/package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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",
Expand Down
24 changes: 22 additions & 2 deletions packages/hap-compiler/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'

/**
Expand Down Expand Up @@ -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'
36 changes: 36 additions & 0 deletions packages/hap-compiler/src/lifecycle/index.js
Original file line number Diff line number Diff line change
@@ -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(`<data> create/update 必须为 Object 对象`)
}

// 校验 params 合法性(规则与 actions params 一致)
checkParams(hookObj.params, 1)

return {
jsonLifecycle: hookObj
}
}

export default {
parse
}
36 changes: 36 additions & 0 deletions packages/hap-compiler/src/lifecycle/validator.js
Original file line number Diff line number Diff line change
@@ -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(`<data> create/update 中 params 参数名不能以 “$” 开头`)
}

if (exp.isExpr(paramsObj[key]) && dep > 1) {
throw new Error(`<data> create/update 中 params 参数值只支持一级结构中绑定变量`)
}

checkParams(paramsObj[key], dep + 1)
})
}

export { checkParams }
23 changes: 22 additions & 1 deletion packages/hap-compiler/src/template/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ function traverse(node, output, previousNode, conditionList, options) {
}
}

// 编译期护栏:禁止在模板表达式中调用耗时 Feature
validator.checkForbiddenFeature(value, locationInfo, options)

switch (name) {
case 'id':
// 保留checkId为兼容原有:新打的RPK包兼容原来的APK平台
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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}`
})
Expand Down
59 changes: 57 additions & 2 deletions packages/hap-compiler/src/template/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {},
Expand All @@ -233,7 +241,7 @@ const tagNatives = {
supportzoom: {
enum: ['true', 'false']
},
intercepturl:{}
intercepturl: {}
}
},
list: {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2150,6 +2204,7 @@ export default {
checkEvent,
checkCustomDirective,
checkAttr,
checkForbiddenFeature,
checkBuild,
checkModel,
isReservedTag,
Expand Down
4 changes: 2 additions & 2 deletions packages/hap-debugger/package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/hap-dev-utils/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
8 changes: 4 additions & 4 deletions packages/hap-dsl-xvm/package.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions packages/hap-dsl-xvm/src/loaders/common/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ export const FRAG_TYPE = {
SCRIPT: 'script',
DATA: 'data',
ACTIONS: 'actions',
CREATE: 'create',
UPDATE: 'update',
PROPS: 'props',
// honor frag
DATAPP: 'app',
Expand Down
18 changes: 18 additions & 0 deletions packages/hap-dsl-xvm/src/loaders/create-loader.js
Original file line number Diff line number Diff line change
@@ -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} 中的 <data> 解析失败,请检查是否为标准的 JSON 格式\n${e}`)
}
return `module.exports = ${createStr}`
}
7 changes: 7 additions & 0 deletions packages/hap-dsl-xvm/src/loaders/template-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions packages/hap-dsl-xvm/src/loaders/update-loader.js
Original file line number Diff line number Diff line change
@@ -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} 中的 <data> 解析失败,请检查是否为标准的 JSON 格式\n${e}`)
}
return `module.exports = ${updateStr}`
}
Loading