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