1/* 2 * Copyright (c) 2021 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16const path = require('path') 17const { Compilation } = require('webpack') 18 19/** 20 * Webpack plugin that processes assets after compilation completes 21 * Modifies assets before final emission, excluding the main ability file 22 */ 23class AfterEmitPlugin { 24 constructor() { 25 } 26 27 /** 28 * Apply method required by webpack plugin interface 29 * @param {Object} compiler - Webpack compiler instance 30 */ 31 apply(compiler) { 32 compiler.hooks.thisCompilation.tap('card', (compilation) => { 33 compilation.hooks.processAssets.tapAsync( 34 { 35 name: 'MyPlugin', 36 stage: Compilation.PROCESS_ASSETS_STAGE_REPOR, 37 }, 38 (assets, back) => { 39 const keys = Object.keys(assets) 40 keys.forEach(key => { 41 if ('./' + process.env.abilityType + '.js' !== key) { 42 sourceChange(key, assets, compilation); 43 } 44 }); 45 back && back(); 46 } 47 ); 48 }) 49 } 50} 51 52/** 53 * Processes JavaScript assets to extract card components and convert them to JSON format 54 * 55 * @param {string} key - Asset file path key 56 * @param {Object} assets - Webpack assets object 57 * @param {Object} compilation - Webpack compilation object 58 */ 59function sourceChange(key, assets, compilation) { 60 try { 61 const extName = path.extname(key); 62 if (extName === '.js') { 63 const jsonOut = {}; 64 const source = assets[key].source(); 65 const str = source.match(/card_start((\s||\S)*)card_end/)[1]; 66 const array = str.split('\n'); 67 array.forEach(element => { 68 elementChange(element, source, jsonOut); 69 }); 70 const assetReplace = {}; 71 Object.keys(jsonOut).forEach(function (jsonOutKey) { 72 toAddJson(assetReplace, jsonOutKey, JSON.parse(jsonOut[jsonOutKey]), compilation, path.join(process.env.projectPath, key)); 73 }); 74 assets[key.replace(extName, '') + '.json'] = { 75 source: function () { 76 return JSON.stringify(assetReplace, null, 2); 77 }, 78 size: function () { 79 return JSON.stringify(assetReplace, null, 2).size; 80 } 81 } 82 delete assets[key]; 83 } 84 } catch (e) { 85 compilation.errors.push({ message: 'errorStartERROR File:' + key + 86 '\n' + ` ${e.message}` + 'errorEnd' }); 87 } 88} 89 90/** 91 * Processes individual element declarations in source code and extracts their values 92 * 93 * @param {string} element - The line of code containing the element declaration 94 * @param {string} source - The full source code of the file 95 * @param {Object} jsonOut - The output object that collects extracted values 96 */ 97function elementChange(element, source, jsonOut) { 98 if (element.trim() === '' || element.trim() === '//') { 99 } else { 100 const key = element.match(/var ((\s||\S)*) =/)[1]; 101 const value = element.match(/"((\s||\S)*)"/)[1]; 102 const replaceSource = source.replace(element, '').toString(); 103 const partStart = replaceSource.indexOf(value); 104 const subSource = replaceSource.substr(partStart); 105 const partEnd = subSource.indexOf('/***/ })'); 106 let out = subSource.substr(0, partEnd).match(/module\.exports \= ((\s||\S)*)/)[1].trim(); 107 if (out.indexOf('JSON.parse(') === 0) { 108 out = JSON.stringify(eval(out)); 109 } 110 if (out.substr(out.length - 1, 1) === ';') { 111 out = out.substr(0, out.length - 1); 112 } 113 jsonOut[key] = out; 114 } 115} 116 117/** 118 * Organizes extracted card components into a structured JSON object 119 * Validates and processes different types of card components 120 * 121 * @param {Object} assetReplace - The output object being built 122 * @param {string} key - The component type key (e.g., 'card_template') 123 * @param {Object} value - The component value to process 124 * @param {Object} compilation - Webpack compilation object for error reporting 125 * @param {string} sourceKey - Source file path for error context 126 */ 127function toAddJson(assetReplace, key, value, compilation, sourceKey) { 128 assetReplace['template'] = assetReplace['template'] || {}; 129 assetReplace['styles'] = assetReplace['styles'] || {}; 130 assetReplace['data'] = assetReplace['data'] || {}; 131 assetReplace['actions'] = assetReplace['actions'] || {}; 132 assetReplace['apiVersion'] = assetReplace['apiVersion'] || {}; 133 134 switch(key) { 135 case 'card_template': 136 assetReplace['template'] = value; 137 break; 138 case 'card_style': 139 assetReplace['styles'] = value; 140 break; 141 case 'card_json': 142 if (value) { 143 if (value.data) { 144 assetReplace['data'] = validateData(value.data, compilation, sourceKey); 145 } 146 if (value.actions) { 147 assetReplace['actions'] = processActions(value.actions, compilation, sourceKey); 148 } 149 if (value.apiVersion) { 150 assetReplace['apiVersion'] = validateData(value.apiVersion, compilation, sourceKey); 151 } 152 if (value.props) { 153 assetReplace['props'] = replacePropsArray(value.propsValue, compilation, sourceKey); 154 } 155 } 156 break; 157 default: 158 addElement(assetReplace, key, value, compilation, sourceKey); 159 break; 160 } 161} 162 163/** 164 * Processes and organizes nested element components into the final JSON structure 165 * Handles templates, styles, and data for individual card elements 166 * 167 * @param {Object} assetReplace - The main output object being constructed 168 * @param {string} key - The element identifier key (e.g., 'card_element_template_header') 169 * @param {Object} value - The element's content to be processed 170 * @param {Object} compilation - Webpack compilation object for error handling 171 * @param {string} sourceKey - Source file path for error context 172 */ 173function addElement(assetReplace, key, value, compilation, sourceKey) { 174 const keyName = key.substr(key.lastIndexOf('_') + 1, key.length - key.lastIndexOf('_') + 1); 175 assetReplace[keyName] = assetReplace[keyName] || {}; 176 assetReplace[keyName]['template'] = assetReplace[keyName]['template'] || {}; 177 assetReplace[keyName]['styles'] = assetReplace[keyName]['styles'] || {}; 178 assetReplace[keyName]['data'] = assetReplace[keyName]['data'] || {}; 179 assetReplace[keyName]['actions'] = assetReplace[keyName]['actions'] || {}; 180 assetReplace[keyName]['apiVersion'] = assetReplace[keyName]['apiVersion'] || {}; 181 182 switch(key.replace(keyName, '')) { 183 case 'card_element_template_': 184 assetReplace[keyName]['template'] = value; 185 break; 186 case 'card_element_style_': 187 assetReplace[keyName]['styles'] = value; 188 break; 189 case 'card_element_json_': 190 if (value) { 191 if (value.data) { 192 assetReplace[keyName]['data'] = validateData(value.data, compilation, sourceKey); 193 } 194 if (value.actions) { 195 assetReplace[keyName]['actions'] = processActions(value.actions, compilation, sourceKey); 196 } 197 if (value.apiVersion) { 198 assetReplace[keyName]['apiVersion'] = validateData(value.apiVersion, compilation, sourceKey); 199 } 200 if (value.props) { 201 assetReplace[keyName]['props'] = replacePropsArray(value.propsValue, compilation, sourceKey); 202 } 203 } 204 break; 205 default: 206 break; 207 } 208} 209 210/** 211 * Normalizes and validates component props configuration for custom elements 212 * Converts array format to object format and ensures proper structure 213 * 214 * @param {Array|Object} propsValue - The props configuration to process 215 * @param {Object} _this - Webpack compilation context for warnings 216 * @param {string} key - Source file path for warning messages 217 * @returns {Object} Normalized props configuration object 218 */ 219function replacePropsArray(propsValue, _this, key) { 220 if (!propsValue) { 221 return propsValue; 222 } 223 if (Array.isArray(propsValue)) { 224 const propsObject = {}; 225 propsValue.forEach(item => { 226 if (typeof(item) !== 'string') { 227 _this.warnings.push({message: 'warnStartWARNING File:' + key + 228 '\n' + `The props value type should be 'string', not '${typeof(item)}' in props array in custom elements.` + 'warnEnd'}); 229 } 230 propsObject[item] = { 'default': '' }; 231 }); 232 propsValue = propsObject; 233 } else if (Object.prototype.toString.call(propsValue) === '[object Object]') { 234 Object.keys(propsValue).forEach(item => { 235 if (Object.prototype.toString.call(propsValue[item]) !== '[object Object]') { 236 _this.warnings.push({message: 'warnStartWARNING File:' + key + 237 '\n' + 'The props default value type can only be Object in custom elements.' + 'warnEnd'}); 238 } 239 if (!propsValue[item].hasOwnProperty('default')) { 240 propsValue[item] = { 'default': '' } 241 } 242 }); 243 } else { 244 _this.warnings.push({message: 'warnStartWARNING File:' + key + 245 '\n' + 'The props type can only be Array or Object in custom elements.' + 'warnEnd'}); 246 } 247 return propsValue; 248} 249 250/** 251 * Validates and normalizes actions configuration for components 252 * Ensures actions follow required format and conventions 253 * 254 * @param {Object} actionsValue - The actions configuration to process 255 * @param {Object} _this - Webpack compilation context for warnings 256 * @param {string} key - Source file path for warning messages 257 * @returns {Object} Validated actions configuration 258 */ 259function processActions(actionsValue, _this, key) { 260 if (Object.prototype.toString.call(actionsValue) === '[object Object]') { 261 Object.keys(actionsValue).forEach(item => { 262 if (actionsValue[item].method) { 263 if (typeof(actionsValue[item].method) === 'string') { 264 if (actionsValue[item].method.toLowerCase() !== actionsValue[item].method) { 265 _this.warnings.push({message: 'warnStartWARNING File:' + key + 266 '\n' + `WARNING: The key method '${actionsValue[item].method}' in the actions don't support uppercase letters.` + 'warnEnd'}); 267 actionsValue[item].method = actionsValue[item].method.toLowerCase(); 268 } 269 } else { 270 _this.warnings.push({message: 'warnStartWARNING File:' + key + 271 '\n' + `WARNING: The key method type in the actions should be 'string', not '${typeof(actionsValue[item].method)}'.` + 'warnEnd'}); 272 } 273 } 274 }) 275 } else { 276 if (actionsValue) { 277 _this.warnings.push({message: 'warnStartWARNING File:' + key + 278 '\n' + 'WARNING: The actions value type can only be Object.' + 'warnEnd'}); 279 } 280 } 281 return actionsValue; 282} 283 284/** 285 * Validates that data configuration is in the correct object format 286 * Issues warnings if invalid data format is detected 287 * 288 * @param {Object} dataValue - The data configuration to validate 289 * @param {Object} _this - Webpack compilation context for warnings 290 * @param {string} key - Source file path for warning messages 291 * @returns {Object} The validated data configuration (unchanged if valid) 292 */ 293function validateData(dataValue, _this, key) { 294 if (dataValue && Object.prototype.toString.call(dataValue) !== '[object Object]') { 295 _this.warnings.push({message: 'warnStartWARNING File:' + key + 296 '\n' + 'WARNING: The data value type can only be Object.' + 'warnEnd'}); 297 } 298 return dataValue; 299} 300 301module.exports = { 302 AfterEmitPlugin: AfterEmitPlugin 303}