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 fs = require('fs'); 17const path = require('path'); 18const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); 19 20const CUSTOM_THEME_PROP_GROUPS = require('./theme/customThemeStyles'); 21const OHOS_THEME_PROP_GROUPS = require('./theme/ohosStyles'); 22import { mkDir } from './util'; 23import { multiResourceBuild } from '../main.product'; 24 25const FILE_EXT_NAME = ['.js', '.css', '.jsx', '.less', '.sass', '.scss', '.md', '.DS_Store', '.hml', '.json']; 26const red = '\u001b[31m'; 27const reset = '\u001b[39m'; 28let input = ''; 29let output = ''; 30let manifestFilePath = ''; 31let watchCSSFiles; 32let shareThemePath = ''; 33let internalThemePath = ''; 34let resourcesPath; 35let sharePath; 36let workerFile; 37 38function copyFile(input, output) { 39 try { 40 if (fs.existsSync(input)) { 41 const parent = path.join(output, '..'); 42 if (!(fs.existsSync(parent) && fs.statSync(parent).isDirectory())) { 43 mkDir(parent); 44 } 45 const pathInfo = path.parse(input); 46 const entryObj = addPageEntryObj(); 47 const indexPath = pathInfo.dir + path.sep + pathInfo.name + '.hml?entry'; 48 for (const key in entryObj) { 49 if (entryObj[key] === indexPath) { 50 return; 51 } 52 } 53 if (pathInfo.ext === '.json' && (pathInfo.dir === shareThemePath || 54 pathInfo.dir === internalThemePath)) { 55 if (themeFileBuild(input, output)) { 56 return; 57 } 58 } 59 const readStream = fs.createReadStream(input); 60 const writeStream = fs.createWriteStream(output); 61 readStream.pipe(writeStream); 62 readStream.on('close', function() { 63 writeStream.end(); 64 }); 65 } 66 } catch (e) { 67 if (e && /ERROR: /.test(e)) { 68 throw e; 69 } else { 70 throw new Error(`${red}Failed to build file ${input}.${reset}`).message; 71 } 72 } 73} 74 75function circularFile(inputPath, outputPath, ext) { 76 const realPath = path.join(inputPath, ext); 77 if (!fs.existsSync(realPath) || realPath === output) { 78 return; 79 } 80 fs.readdirSync(realPath).forEach(function(file_) { 81 const file = path.join(realPath, file_); 82 const fileStat = fs.statSync(file); 83 if (fileStat.isFile()) { 84 const baseName = path.basename(file); 85 const extName = path.extname(file); 86 const outputFile = path.join(outputPath, ext, path.basename(file_)); 87 if (outputFile === path.join(output, 'manifest.json')) { 88 return; 89 } 90 if (FILE_EXT_NAME.indexOf(extName) < 0 && baseName !== '.DS_Store') { 91 toCopyFile(file, outputFile, fileStat) 92 } else if (extName === '.json') { 93 const parent = path.join(file, '..'); 94 const parent_ = path.join(parent, '..'); 95 if (path.parse(parent).name === 'i18n' && path.parse(parent_).name === 'share') { 96 toCopyFile(file, outputFile, fileStat) 97 } else if (file.toString().startsWith(resourcesPath) || file.toString().startsWith(sharePath)) { 98 toCopyFile(file, outputFile, fileStat) 99 } 100 } 101 } else if (fileStat.isDirectory()) { 102 circularFile(inputPath, outputPath, path.join(ext, file_)); 103 } 104 }); 105} 106 107function toCopyFile(file, outputFile, fileStat) { 108 if (fs.existsSync(outputFile)) { 109 const outputFileStat = fs.statSync(outputFile); 110 if (outputFileStat.isFile() && fileStat.size !== outputFileStat.size) { 111 copyFile(file, outputFile); 112 } 113 } else { 114 copyFile(file, outputFile); 115 } 116} 117 118class ResourcePlugin { 119 constructor(input_, output_, manifestFilePath_, watchCSSFiles_, workerFile_ = null) { 120 input = input_; 121 output = output_; 122 manifestFilePath = manifestFilePath_; 123 watchCSSFiles = watchCSSFiles_; 124 shareThemePath = path.join(input_, '../share/resources/styles'); 125 internalThemePath = path.join(input_, 'resources/styles'); 126 resourcesPath = path.resolve(input_, 'resources'); 127 sharePath = path.resolve(input_, '../share/resources'); 128 workerFile = workerFile_; 129 } 130 apply(compiler) { 131 compiler.hooks.beforeCompile.tap('resource Copy', () => { 132 if (multiResourceBuild.value) { 133 const res = multiResourceBuild.value.res; 134 if (res) { 135 res.forEach(item => { 136 circularFile(path.resolve(input, item), path.resolve(output, item), ''); 137 }) 138 } 139 } else { 140 circularFile(input, output, ''); 141 } 142 circularFile(input, output, '../share'); 143 }); 144 compiler.hooks.watchRun.tap('i18n', (comp) => { 145 checkRemove(comp); 146 }); 147 compiler.hooks.normalModuleFactory.tap('OtherEntryOptionPlugin', () => { 148 if (process.env.abilityType === 'testrunner') { 149 checkTestRunner(input, entryObj); 150 for (const key in entryObj) { 151 const singleEntry = new SingleEntryPlugin('', entryObj[key], key); 152 singleEntry.apply(compiler); 153 } 154 } else { 155 const cssFiles = readCSSInfo(watchCSSFiles); 156 if (process.env.DEVICE_LEVEL === 'card' && process.env.compileMode !== 'moduleJson') { 157 for(const entryKey in compiler.options.entry) { 158 setCSSEntry(cssFiles, entryKey); 159 } 160 writeCSSInfo(watchCSSFiles, cssFiles); 161 return; 162 } 163 addPageEntryObj(); 164 entryObj = Object.assign(entryObj, abilityEntryObj); 165 for (const key in entryObj) { 166 if (!compiler.options.entry[key]) { 167 const singleEntry = new SingleEntryPlugin('', entryObj[key], key); 168 singleEntry.apply(compiler); 169 } 170 setCSSEntry(cssFiles, key); 171 } 172 writeCSSInfo(watchCSSFiles, cssFiles); 173 } 174 }); 175 compiler.hooks.done.tap('copyManifest', () => { 176 copyManifest(); 177 }); 178 } 179} 180 181function checkRemove(comp) { 182 const removedFiles = comp.removedFiles || []; 183 removedFiles.forEach(file => { 184 if (file.indexOf(process.env.projectPath) > -1 && path.extname(file) === '.json' && 185 file.indexOf('i18n') > -1) { 186 const buildFilePath = file.replace(process.env.projectPath, process.env.buildPath); 187 if (fs.existsSync(buildFilePath)) { 188 fs.unlinkSync(buildFilePath); 189 } 190 } 191 }) 192} 193 194function copyManifest() { 195 copyFile(manifestFilePath, path.join(output, 'manifest.json')); 196 if (fs.existsSync(path.join(output, 'app.js'))) { 197 fs.utimesSync(path.join(output, 'app.js'), new Date(), new Date()); 198 } 199} 200 201let entryObj = {}; 202let configPath; 203 204function addPageEntryObj() { 205 entryObj = {}; 206 if (process.env.abilityType === 'page') { 207 let jsonContent; 208 if (multiResourceBuild.value && Object.keys(multiResourceBuild.value).length) { 209 jsonContent = multiResourceBuild.value; 210 } else { 211 jsonContent = readManifest(manifestFilePath); 212 } 213 const pages = jsonContent.pages; 214 if (pages === undefined) { 215 throw Error('ERROR: missing pages').message; 216 } 217 pages.forEach((element) => { 218 const sourcePath = element.replace(/^\.\/js\//, ''); 219 const hmlPath = path.join(input, sourcePath + '.hml'); 220 const aceSuperVisualPath = process.env.aceSuperVisualPath || ''; 221 const visualPath = path.join(aceSuperVisualPath, sourcePath + '.visual'); 222 const isHml = fs.existsSync(hmlPath); 223 const isVisual = fs.existsSync(visualPath); 224 const projectPath = process.env.projectPath; 225 if (isHml && isVisual) { 226 console.error('ERROR: ' + sourcePath + ' cannot both have hml && visual'); 227 } else if (isHml) { 228 entryObj['./' + sourcePath] = path.resolve(projectPath, './' + sourcePath + '.hml?entry'); 229 } else if (isVisual) { 230 entryObj['./' + sourcePath] = path.resolve(aceSuperVisualPath, './' + sourcePath + 231 '.visual?entry'); 232 } else { 233 entryErrorLog(sourcePath); 234 } 235 }); 236 } 237 if (process.env.isPreview !== 'true' && process.env.DEVICE_LEVEL === 'rich') { 238 loadWorker(entryObj); 239 } 240 return entryObj; 241} 242 243function entryErrorLog(sourcePath) { 244 if (process.env.watchMode && process.env.watchMode === 'true') { 245 console.error('COMPILE RESULT:FAIL '); 246 console.error('ERROR: Invalid route ' + sourcePath + 247 '. Verify the route infomation' + (configPath ? " in the " + configPath : '') + 248 ', and then restart the Previewer.'); 249 return; 250 } else { 251 throw Error( 252 '\u001b[31m' + 'ERROR: Invalid route ' + sourcePath + 253 '. Verify the route infomation' + (configPath ? " in the " + configPath : '') + 254 ', and then restart the Build.').message; 255 } 256} 257 258let abilityEntryObj = {}; 259function addAbilityEntryObj(moduleJson) { 260 abilityEntryObj = {}; 261 const entranceFiles = readAbilityEntrance(moduleJson); 262 entranceFiles.forEach(filePath => { 263 const key = filePath.replace(/^\.\/js\//, './').replace(/\.js$/, ''); 264 abilityEntryObj[key] = path.resolve(process.env.projectPath, '../', filePath); 265 }); 266 return abilityEntryObj; 267} 268 269function readAbilityEntrance(moduleJson) { 270 const entranceFiles = []; 271 if (moduleJson.module) { 272 if (moduleJson.module.srcEntrance) { 273 entranceFiles.push(moduleJson.module.srcEntrance); 274 } 275 if (moduleJson.module.abilities && moduleJson.module.abilities.length) { 276 readEntrances(moduleJson.module.abilities, entranceFiles); 277 } 278 if (moduleJson.module.extensionAbilities && moduleJson.module.extensionAbilities.length) { 279 readEntrances(moduleJson.module.extensionAbilities, entranceFiles); 280 } 281 } 282 return entranceFiles; 283} 284 285function readEntrances(abilities, entranceFiles) { 286 abilities.forEach(ability => { 287 if ((!ability.type || ability.type !== 'form') && ability.srcEntrance) { 288 entranceFiles.push(ability.srcEntrance); 289 } 290 }); 291} 292 293function readManifest(manifestFilePath) { 294 let manifest = {}; 295 try { 296 if (fs.existsSync(manifestFilePath)) { 297 configPath = manifestFilePath; 298 const jsonString = fs.readFileSync(manifestFilePath).toString(); 299 manifest = JSON.parse(jsonString); 300 } else if (process.env.aceModuleJsonPath && fs.existsSync(process.env.aceModuleJsonPath)) { 301 buildManifest(manifest); 302 } else { 303 throw Error('\u001b[31m' + 'ERROR: the manifest.json or module.json is lost.' + 304 '\u001b[39m').message; 305 } 306 process.env.minPlatformVersion = manifest.minPlatformVersion; 307 } catch (e) { 308 throw Error('\u001b[31m' + 'ERROR: the manifest.json or module.json file format is invalid.' + 309 '\u001b[39m').message; 310 } 311 return manifest; 312} 313 314function readModulePages(moduleJson) { 315 if (moduleJson.module.pages) { 316 const modulePagePath = path.resolve(process.env.aceProfilePath, 317 `${moduleJson.module.pages.replace(/\$profile\:/, '')}.json`); 318 if (fs.existsSync(modulePagePath)) { 319 configPath = modulePagePath; 320 const pagesConfig = JSON.parse(fs.readFileSync(modulePagePath, 'utf-8')); 321 return pagesConfig.src; 322 } 323 } 324} 325 326function readFormPages(moduleJson) { 327 const pages = []; 328 if (moduleJson.module.extensionAbilities && moduleJson.module.extensionAbilities.length) { 329 moduleJson.module.extensionAbilities.forEach(extensionAbility => { 330 if (extensionAbility.type && extensionAbility.type === 'form' && extensionAbility.metadata && 331 extensionAbility.metadata.length) { 332 extensionAbility.metadata.forEach(item => { 333 if (item.resource && /\$profile\:/.test(item.resource)) { 334 parseFormConfig(item.resource, pages); 335 } 336 }); 337 } 338 }); 339 } 340 return pages; 341} 342 343function parseFormConfig(resource, pages) { 344 const resourceFile = path.resolve(process.env.aceProfilePath, 345 `${resource.replace(/\$profile\:/, '')}.json`); 346 if (fs.existsSync(resourceFile)) { 347 configPath = resourceFile; 348 const pagesConfig = JSON.parse(fs.readFileSync(resourceFile, 'utf-8')); 349 if (pagesConfig.forms && pagesConfig.forms.length) { 350 pagesConfig.forms.forEach(form => { 351 if (form.src && (form.uiSyntax === 'hml' || !form.uiSyntax)) { 352 pages.push(form.src); 353 } 354 }); 355 } 356 } 357} 358 359function buildManifest(manifest) { 360 try { 361 const moduleJson = JSON.parse(fs.readFileSync(process.env.aceModuleJsonPath).toString()); 362 if (process.env.DEVICE_LEVEL === 'rich') { 363 manifest.pages = readModulePages(moduleJson); 364 manifest.abilityEntryObj = addAbilityEntryObj(moduleJson); 365 } 366 if (process.env.DEVICE_LEVEL === 'card') { 367 manifest.pages = readFormPages(moduleJson); 368 } 369 manifest.minPlatformVersion = moduleJson.app.minAPIVersion; 370 } catch (e) { 371 throw Error("\x1B[31m" + 'ERROR: the module.json file is lost or format is invalid.' + 372 "\x1B[39m").message; 373 } 374} 375 376function loadWorker(entryObj) { 377 if (workerFile) { 378 entryObj = Object.assign(entryObj, workerFile); 379 } else { 380 const workerPath = path.resolve(input, 'workers'); 381 if (fs.existsSync(workerPath)) { 382 const workerFiles = []; 383 readFile(workerPath, workerFiles); 384 workerFiles.forEach((item) => { 385 if (/\.js$/.test(item)) { 386 const relativePath = path.relative(workerPath, item) 387 .replace(/\.js$/, '').replace(/\\/g, '/'); 388 entryObj[`./workers/` + relativePath] = item; 389 } 390 }); 391 } 392 } 393} 394 395function validateWorkOption() { 396 if (process.env.aceBuildJson && fs.existsSync(process.env.aceBuildJson)) { 397 const workerConfig = JSON.parse(fs.readFileSync(process.env.aceBuildJson).toString()); 398 if(workerConfig.workers) { 399 return true; 400 } 401 } 402 return false; 403} 404 405function filterWorker(workerPath) { 406 return /\.(ts|js)$/.test(workerPath); 407} 408 409function readFile(dir, utFiles) { 410 try { 411 const files = fs.readdirSync(dir); 412 files.forEach((element) => { 413 const filePath = path.join(dir, element); 414 const status = fs.statSync(filePath); 415 if (status.isDirectory()) { 416 readFile(filePath, utFiles); 417 } else { 418 utFiles.push(filePath); 419 } 420 }); 421 } catch (e) { 422 console.error(e.message); 423 } 424} 425 426function themeFileBuild(customThemePath, customThemeBuild) { 427 if (fs.existsSync(customThemePath)) { 428 const themeContent = JSON.parse(fs.readFileSync(customThemePath)); 429 const newContent = {}; 430 if (themeContent && themeContent['style']) { 431 newContent['style'] = {}; 432 const styleContent = themeContent['style']; 433 Object.keys(styleContent).forEach(function(key) { 434 const customKey = CUSTOM_THEME_PROP_GROUPS[key]; 435 const ohosKey = OHOS_THEME_PROP_GROUPS[key]; 436 if (ohosKey) { 437 newContent['style'][ohosKey] = styleContent[key]; 438 } else if (customKey) { 439 newContent['style'][customKey] = styleContent[key]; 440 } else { 441 newContent['style'][key] = styleContent[key]; 442 } 443 }); 444 fs.writeFileSync(customThemeBuild, JSON.stringify(newContent, null, 2)); 445 return true; 446 } 447 } 448 return false; 449} 450 451function readCSSInfo(watchCSSFiles) { 452 if (fs.existsSync(watchCSSFiles)) { 453 return JSON.parse(fs.readFileSync(watchCSSFiles)); 454 } else { 455 return {}; 456 } 457} 458 459function writeCSSInfo(filePath, infoObject) { 460 if (!(process.env.tddMode === 'true') && !(fs.existsSync(path.resolve(filePath, '..')) && 461 fs.statSync(path.resolve(filePath, '..')).isDirectory())) { 462 mkDir(path.resolve(filePath, '..')); 463 } 464 if (fs.existsSync(filePath)) { 465 fs.unlinkSync(filePath); 466 } 467 if (fs.existsSync(path.resolve(filePath, '..')) && fs.statSync(path.resolve(filePath, '..')).isDirectory()) { 468 fs.writeFileSync(filePath, JSON.stringify(infoObject, null, 2)); 469 } 470} 471 472function setCSSEntry(cssfiles, key) { 473 cssfiles["entry"] = cssfiles["entry"] || {}; 474 if (fs.existsSync(path.join(input, key + '.css'))) { 475 cssfiles["entry"][path.join(input, key + '.css')] = true; 476 } 477} 478 479function checkTestRunner(projectPath, entryObj) { 480 const files = []; 481 walkSync(projectPath, files, entryObj, projectPath); 482} 483 484function walkSync(filePath_, files, entryObj, projectPath) { 485 fs.readdirSync(filePath_).forEach(function (name) { 486 const filePath = path.join(filePath_, name); 487 const stat = fs.statSync(filePath); 488 if (stat.isFile()) { 489 const extName = '.js'; 490 if (path.extname(filePath) === extName) { 491 const key = filePath.replace(projectPath, '').replace(extName, ''); 492 entryObj[`./${key}`] = filePath; 493 } else if (stat.isDirectory()) { 494 walkSync(filePath, files, entryObj, projectPath); 495 } 496 } 497 }) 498} 499 500module.exports = ResourcePlugin; 501