1/* 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, 13 * software distributed under the License is distributed on an 14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 * KIND, either express or implied. See the License for the 16 * specific language governing permissions and limitations 17 * under the License. 18 */ 19 20import loaderUtils from 'loader-utils' 21import path from 'path' 22import fs from 'fs' 23 24import * as legacy from './legacy' 25import { 26 parseFragment 27} 28from './parser' 29import { 30 getNameByPath, 31 getRequireString, 32 stringifyLoaders, 33 logWarn, 34 loadBabelModule, 35 elements 36} 37from './util' 38import { isReservedTag } from './templater/component_validator' 39 40const { DEVICE_LEVEL } = require('./lite/lite-enum') 41const loaderPath = __dirname 42const defaultLoaders = { 43 none: '', 44 main: path.resolve(loaderPath, 'loader.js'), 45 template: path.resolve(loaderPath, 'template.js'), 46 style: path.resolve(loaderPath, 'style.js'), 47 script: path.resolve(loaderPath, 'script.js'), 48 json: path.resolve(loaderPath, 'json.js'), 49 babel: loadBabelModule('babel-loader'), 50 manifest: path.resolve(loaderPath, 'manifest-loader.js'), 51 resourceReferenceScript: path.resolve(loaderPath, 'resource-reference-script.js') 52} 53 54/** 55 * Central loader configuration factory that returns the appropriate loader string 56 * based on file type and configuration. Acts as a dispatcher for specialized loader strings. 57 * 58 * @param {string} type - The type of loader needed (main/element/template/style/script/config/data) 59 * @param {Object} config - Configuration options for the loader including: 60 * - lang: Language specification 61 * - customLang: Custom language loaders 62 * - source: Source file path 63 * - app: Boolean flag for application scripts 64 * @returns {string} Webpack-compatible loader string 65 */ 66function getLoaderString (type, config) { 67 config = config || {} 68 const customLoader = loadCustomLoader(config) 69 let loaders 70 switch (type) { 71 case 'main': 72 return mainLoaderString(loaders) 73 case 'element': 74 return elementLoaderString(loaders, config) 75 case 'template': 76 return templateLoaderString(loaders, config, customLoader) 77 case 'style': 78 return styleLoaderString(loaders, config, customLoader) 79 case 'script': 80 return scriptLoaderString(loaders, config, customLoader) 81 case 'config': 82 return configLoaderString(loaders, config) 83 case 'data': 84 return dataLoaderString(loaders, config) 85 } 86} 87 88/** 89 * Loads a custom language loader based on configuration 90 * Primarily used for loading Babel-compatible language processors (like TypeScript, Flow, etc.) 91 * 92 * @param {Object} config - Configuration object containing: 93 * - lang: The language to load (e.g., 'typescript', 'flow') 94 * - customLang: Map of available custom languages and their loaders 95 * @returns {Function|undefined} The loaded custom loader function, or undefined if not found 96 */ 97function loadCustomLoader (config) { 98 if (config.lang && config.customLang[config.lang]) { 99 return loadBabelModule(config.customLang[config.lang][0]) 100 } 101} 102 103/** 104 * Generates a webpack loader string using the default main loader 105 * Provides a clean way to get the standard main loader configuration 106 * 107 * @param {Array} loaders - Initial loader array (will be overridden) 108 * @returns {string} Webpack-compatible loader string with just the main loader 109 */ 110function mainLoaderString (loaders) { 111 loaders = [{ 112 name: defaultLoaders.main 113 }] 114 return stringifyLoaders(loaders) 115} 116 117/** 118 * Generates a webpack loader string for processing custom elements/components 119 * Configures the main loader with element-specific options 120 * 121 * @param {Array} loaders - Initial loader array (will be overridden) 122 * @param {Object} config - Configuration options including: 123 * - source: Source file path (optional) 124 * @returns {string} Webpack-compatible loader string 125 */ 126function elementLoaderString (loaders, config) { 127 loaders = [{ 128 name: defaultLoaders.main, 129 query: { 130 element: config.source ? undefined : true 131 } 132 }] 133 return stringifyLoaders(loaders) 134} 135 136/** 137 * Generates a webpack loader string for processing template files 138 * Combines default JSON parsing with template processing, plus optional custom loaders 139 * 140 * @param {Array} loaders - Initial loader array (will be overridden) 141 * @param {Object} config - Configuration options (currently unused) 142 * @param {Array} customLoader - Optional custom loaders to append 143 * @returns {string} Webpack-compatible loader string 144 */ 145function templateLoaderString (loaders, config, customLoader) { 146 loaders = [{ 147 name: defaultLoaders.json 148 }, { 149 name: defaultLoaders.template 150 }] 151 if (customLoader) { 152 loaders = loaders.concat(customLoader) 153 } 154 return stringifyLoaders(loaders) 155} 156 157/** 158 * Generates a webpack loader string for processing style files (CSS/LESS/SASS/etc) 159 * Combines default JSON and style loaders with optional custom loaders 160 * 161 * @param {Array} loaders - Initial loader configuration (will be overridden) 162 * @param {Object} config - Configuration options (unused in current implementation) 163 * @param {Array} customLoader - Optional array of custom loaders to append 164 * @returns {string} Webpack-compatible loader string 165 */ 166function styleLoaderString (loaders, config, customLoader) { 167 loaders = [{ 168 name: defaultLoaders.json 169 }, { 170 name: defaultLoaders.style 171 }] 172 if (customLoader) { 173 loaders = loaders.concat(customLoader) 174 } 175 return stringifyLoaders(loaders) 176} 177 178/** 179 * Generates a webpack loader string for JavaScript/script files with configurable options 180 * Supports custom loaders, Babel transpilation, and manifest processing for apps 181 * 182 * @param {Array} loaders - Initial loader configuration (will be overridden) 183 * @param {Object} config - Configuration options including: 184 * - app: boolean indicating if processing app script 185 * - source: source file path 186 * @param {Array} customLoader - Optional custom loaders to include 187 * @returns {string} Webpack-compatible loader string 188 */ 189function scriptLoaderString (loaders, config, customLoader) { 190 loaders = [{ 191 name: defaultLoaders.script 192 }] 193 if (customLoader) { 194 loaders = loaders.concat(customLoader) 195 } 196 else { 197 const isTargets = { 198 'extends': path.resolve(__dirname, "../babel.config.js") 199 } 200 if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH) { 201 isTargets['targets'] = 'node 8'; 202 } 203 loaders.push({ 204 name: defaultLoaders.babel, 205 query: isTargets, 206 }) 207 loaders.push({ 208 name: defaultLoaders.resourceReferenceScript 209 }) 210 } 211 if (config.app && process.env.abilityType === 'page' && 212 fs.existsSync(process.env.aceManifestPath)) { 213 loaders.push({ 214 name: defaultLoaders.manifest, 215 query: { 216 path: config.source 217 } 218 }) 219 } 220 return stringifyLoaders(loaders) 221} 222 223/** 224 * Generates a webpack loader string specifically for configuration files (e.g., JSON configs) 225 * Defaults to using the standard JSON loader regardless of input 226 * 227 * @param {Array} loaders - Original loader array (gets overridden) 228 * @param {Object} config - Additional configuration options (currently unused) 229 * @returns {string} Webpack-compatible loader string 230 */ 231function configLoaderString (loaders, config) { 232 loaders = [{ 233 name: defaultLoaders.json 234 }] 235 return stringifyLoaders(loaders) 236} 237 238/** 239 * Generates a loader string for processing data files (e.g., JSON) 240 * Uses default JSON loader configuration and stringifies the loader chain 241 * 242 * @param {Array} loaders - Original loader configuration (overridden in this function) 243 * @param {Object} config - Additional configuration object (unused in current implementation) 244 * @returns {string} Stringified loader configuration 245 */ 246function dataLoaderString (loaders, config) { 247 loaders = [{ 248 name: defaultLoaders.json 249 }] 250 return stringifyLoaders(loaders) 251} 252 253/** 254 * Webpack loader function that processes application and page components 255 * Handles both entry files and child components with proper dependency tracking 256 * 257 * @param {string} source - The source code of the file being processed 258 * @returns {string} The processed output code 259 */ 260function loader (source) { 261 this.cacheable && this.cacheable() 262 263 const options = { 264 lang: { 265 sass:['sass-loader'], 266 scss:['sass-loader'], 267 less:['less-loader'] 268 } 269 } 270 const customLang = options.lang || {} 271 const resourceQuery = this.resourceQuery && loaderUtils.parseQuery(this.resourceQuery) || {} 272 const isEntry = resourceQuery.entry 273 const dirName = path.parse(this.resourcePath) 274 const name = isEntry ? dirName.name : resourceQuery.name || getNameByPath(this.resourcePath) 275 let parentPath = resourceQuery.parentPath || this.resourcePath; 276 if (isEntry) { 277 elements[this.resourcePath] = elements[this.resourcePath] || {}; 278 elements[this.resourcePath][name] = true; 279 } else { 280 elements[this.resourcePath] = elements[this.resourcePath] || {}; 281 elements[this.resourcePath]["parent"] = parentPath; 282 if (elements[parentPath] && elements[parentPath]["parent"]) { 283 elements[this.resourcePath]["parent"] = elements[elements[parentPath]["parent"]]; 284 parentPath = elements[this.resourcePath]["parent"]; 285 } 286 } 287 if (isReservedTag(name) && process.env.abilityType === 'page') { 288 logWarn(this, [{ 289 reason: 'ERROR: The file name cannot contain reserved tag name: ' + name 290 }]) 291 return '' 292 } 293 let output = '' 294 // import app.js 295 output += loadApp(this, name, isEntry, customLang, source) 296 output += loadPage(this, name, isEntry, customLang, source, parentPath); 297 return output 298} 299 300/** 301 * Determines if the current resource is the main application file 302 * 303 * @param {Object} _this - Webpack loader context 304 * @returns {boolean} True if the file is the main application file, false otherwise 305 */ 306function checkApp(_this) { 307 if (process.env.abilityType === 'testrunner') { 308 return true; 309 } 310 return _this.resourcePath === path.resolve(process.env.projectPath, 311 process.env.abilityType === 'page' ? 'app.js' : `${process.env.abilityType}.js`) 312} 313 314/** 315 * Loads and processes an application entry file (app.js) 316 * Handles both Rich and Lite device levels, including CSS and script loading 317 * 318 * @param {Object} _this - Webpack loader context 319 * @param {string} name - Application name 320 * @param {boolean} isEntry - Whether this is an entry point 321 * @param {string} customLang - Custom language setting 322 * @param {string} source - Source content of the file 323 * @returns {string} Generated output code for the application 324 */ 325function loadApp (_this, name, isEntry, customLang, source) { 326 let output = '' 327 let extcss = false 328 if (checkApp(_this)) { 329 const filename = _this.resourcePath.replace(path.extname(_this.resourcePath).toString(), '') 330 // find css 331 const cssFileName = filename + '.css' 332 if (!fs.existsSync(cssFileName)) { 333 extcss = false 334 } 335 else { 336 extcss = true 337 output += 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', { 338 customLang, 339 lang: undefined, 340 element: undefined, 341 elementName: undefined, 342 source: cssFileName 343 }), cssFileName) 344 } 345 output += 'var $app_script$ = ' + getRequireString(_this, getLoaderString('script', { 346 customLang, 347 lang: undefined, 348 element: undefined, 349 elementName: undefined, 350 source: _this.resourcePath, 351 app: true 352 }), _this.resourcePath) 353 354 if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH || process.env.DEVICE_LEVEL === 'card') { 355 output += ` 356 $app_define$('@app-application/${name}', [], function($app_require$, $app_exports$, $app_module$) { 357 ` + ` 358 $app_script$($app_module$, $app_exports$, $app_require$) 359 if ($app_exports$.__esModule && $app_exports$.default) { 360 $app_module$.exports = $app_exports$.default 361 } 362 ` + (extcss ? ` 363 $app_module$.exports.style = $app_style$ 364 ` : '') 365 + ` 366 }) 367 ` 368 if (isEntry) { 369 output += `$app_bootstrap$('@app-application/${name}'` + ',undefined' + ',undefined' + `)` 370 } 371 } 372 if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.LITE) { 373 output += `var options=$app_script$\n if ($app_script$.__esModule) {\n 374 options = $app_script$.default;\n }\n` + 375 (extcss ? `options.styleSheet=$app_style$\n` : ``) + 376 `module.exports=new ViewModel(options);` 377 } 378 return output 379 } else if (/\.js$/.test(_this.resourcePath)) { 380 return source 381 } else { 382 return output 383 } 384} 385 386/** 387 * Main function for loading and processing a page/component 388 * Coordinates the loading of all associated resources (template, CSS, JS) 389 * and generates the final output code for either Rich or Lite device level 390 * 391 * @param {Object} _this - Webpack loader context 392 * @param {string} name - Name of the component/page 393 * @param {boolean} isEntry - Whether this is an entry point 394 * @param {string} customLang - Custom language setting 395 * @param {string} source - Source content of the file 396 * @param {string} parentPath - Path of the parent component 397 * @returns {string} Generated output code for the component 398 */ 399function loadPage (_this, name, isEntry, customLang, source, parentPath) { 400 let output = '' 401 if (path.extname(_this.resourcePath).match(/\.hml/)) { 402 const filename = _this.resourcePath.replace(path.extname(_this.resourcePath).toString(), '') 403 const resourcePath = _this.resourcePath 404 const loaderQuery = loaderUtils.getOptions(_this) || {} 405 const isElement = loaderQuery.element 406 const frag = parseFragment(source) 407 const elementNames = [] 408 const elementLength = frag.element.length 409 output += loadPageCheckElementLength(_this, elementLength, frag, elementNames, resourcePath, 410 customLang, parentPath); 411 412 output += 'var $app_template$ = ' + getRequireString(_this, getLoaderString('template', { 413 customLang, 414 lang: undefined, 415 element: isElement, 416 elementName: isElement ? name : undefined, 417 source: _this.resourcePath 418 }), _this.resourcePath) 419 420 // find css 421 const cssContent = loadPageFindCss(_this, filename, customLang) 422 const extcss = cssContent.extcss 423 output += cssContent.output 424 425 // find js 426 const scriptContent = loadPageFindJs(_this, filename, customLang) 427 const extscript = scriptContent.extscript 428 output += scriptContent.output 429 430 output += process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH ? loadPageCheckRich(name, extscript, extcss, isEntry) : 431 loadPageCheckLite(extscript, extcss) 432 return output 433 } 434 return output 435} 436 437/** 438 * Processes custom elements in a template and generates corresponding require statements 439 * Validates element configurations and checks for naming conflicts 440 * 441 * @param {Object} _this - Webpack compilation context 442 * @param {number} elementLength - Number of custom elements to process 443 * @param {Object} frag - Fragment containing element definitions 444 * @param {Array} elementNames - Array to collect processed element names 445 * @param {string} resourcePath - Path of the parent resource 446 * @param {string} customLang - Custom language setting for loaders 447 * @param {string} parentPath - Path of the parent component 448 * @returns {string} Generated require statements for all valid elements 449 */ 450function loadPageCheckElementLength (_this, elementLength, frag, elementNames, resourcePath, 451 customLang, parentPath) { 452 let output = '' 453 if (elementLength) { 454 for (let i = 0; i < elementLength; i++) { 455 const element = frag.element[i] 456 let src = resourcePath 457 if (element.src) { 458 src = element.src 459 if (!src.match(/\.hml$/)) { 460 src = src.concat('.hml') 461 } 462 const filePath = path.join(path.dirname(resourcePath), src) 463 if (!fs.existsSync(filePath) && src.match(/^(\/|\.)/)) { 464 logWarn(_this, [{ 465 reason: 'ERROR: The file path of custom element does not exist, src: ' + src 466 }]) 467 return '' 468 } 469 if (!element.name) { 470 element.name = path.parse(src).name 471 } 472 element.name = element.name.toLowerCase(); 473 elements[parentPath] = elements[parentPath] || {}; 474 if (elements[parentPath][element.name]) { 475 logWarn(_this, [{ 476 reason: `ERROR: The element name can not be same with the page ` + 477 `"${element.name}" (ignore case).` 478 }]); 479 } else { 480 elements[parentPath][element.name] = true; 481 } 482 checkEntry(_this, filePath, element.src) 483 } 484 else { 485 logWarn(_this, [{ 486 reason: 'ERROR: src attributes must be set for custom elements.' 487 }]) 488 return '' 489 } 490 elementNames.push(element.name) 491 output += getRequireString(_this, getLoaderString('element', { 492 customLang, 493 name: element.name, 494 source: src 495 }), `${src}?name=${element.name}&parentPath=${parentPath}`) 496 } 497 } 498 return output 499} 500 501/** 502 * Checks for and loads CSS or preprocessor style files associated with a component/page 503 * Supports CSS, LESS, SCSS, and SASS file formats 504 * Generates the require statement for the style file if found 505 * 506 * @param {Object} _this - Webpack compilation context 507 * @param {string} filename - Base filename (without extension) 508 * @param {string} customLang - Custom language setting for the loader 509 * @returns {Object} Returns an object containing: 510 * - extcss: boolean indicating if any style file exists 511 * - output: generated require statement or empty string 512 */ 513function loadPageFindCss (_this, filename, customLang) { 514 let output = '' 515 let extcss = false 516 const cssFileName = filename + '.css' 517 if (fs.existsSync(cssFileName)) { 518 extcss = true 519 output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', { 520 customLang, 521 lang: undefined, 522 element: undefined, 523 elementName: undefined, 524 source: cssFileName 525 }), cssFileName) 526 } 527 else { 528 // find less 529 const lessFileName = filename + '.less' 530 if (fs.existsSync(lessFileName)) { 531 extcss = true 532 output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', { 533 customLang, 534 lang: 'less', 535 element: undefined, 536 elementName: undefined, 537 source: lessFileName 538 }), lessFileName) 539 } 540 else { 541 // find scss 542 const scssFileName = filename + '.scss' 543 if (fs.existsSync(scssFileName)) { 544 extcss = true 545 output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', { 546 customLang, 547 lang: 'scss', 548 element: undefined, 549 elementName: undefined, 550 source: scssFileName 551 }), scssFileName) 552 } 553 else { 554 // find sass 555 const sassFileName = filename + '.sass' 556 if (fs.existsSync(sassFileName)) { 557 extcss = true 558 output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', { 559 customLang, 560 lang: 'sass', 561 element: undefined, 562 elementName: undefined, 563 source: sassFileName 564 }), sassFileName) 565 } 566 else { 567 extcss = false 568 } 569 } 570 } 571 } 572 return { 573 extcss: extcss, 574 output: output 575 } 576} 577 578/** 579 * Checks for and loads a JavaScript file associated with a component/page 580 * Generates the require statement for the JS file if it exists 581 * 582 * @param {Object} _this - Webpack compilation context 583 * @param {string} filename - Base filename (without extension) 584 * @param {string} customLang - Custom language setting for the loader 585 * @returns {Object} Returns an object containing: 586 * - extscript: boolean indicating if JS file exists 587 * - output: generated require statement or empty string 588 */ 589function loadPageFindJs (_this, filename, customLang) { 590 let output = '' 591 let extscript = false 592 const jsFileName = filename + '.js' 593 if (!fs.existsSync(jsFileName)) { 594 extscript = false 595 console.log('missing ' + jsFileName) 596 } 597 else { 598 extscript = true 599 output = 'var $app_script$ = ' + getRequireString(_this, getLoaderString('script', { 600 customLang, 601 lang: undefined, 602 element: undefined, 603 elementName: undefined, 604 source: jsFileName 605 }), jsFileName) 606 } 607 return { 608 extscript: extscript, 609 output: output 610 } 611} 612 613/** 614 * Generates component initialization code for Rich mode 615 * Creates a component definition with optional script and style, and handles entry point bootstrapping 616 * 617 * @param {string} name - Component name 618 * @param {boolean} extscript - Whether the component has an external script 619 * @param {boolean} extcss - Whether the component has external CSS 620 * @param {boolean} isEntry - Whether this is an entry component 621 * @returns {string} Generated component definition and bootstrap code 622 */ 623function loadPageCheckRich (name, extscript, extcss, isEntry) { 624 let output = '' 625 output += ` 626$app_define$('@app-component/${name}', [], function($app_require$, $app_exports$, $app_module$) { 627` + (extscript ? ` 628$app_script$($app_module$, $app_exports$, $app_require$) 629if ($app_exports$.__esModule && $app_exports$.default) { 630$app_module$.exports = $app_exports$.default 631} 632` : '') + ` 633$app_module$.exports.template = $app_template$ 634` + (extcss ? ` 635$app_module$.exports.style = $app_style$ 636` : '') + ` 637}) 638` 639 if (isEntry) { 640 output += `$app_bootstrap$('@app-component/${name}'` + ',undefined' + ',undefined' + `)` 641 } 642 return output 643} 644 645/** 646 * Generates the page initialization code for Lite mode 647 * Combines script, style, and template components into a ViewModel instance 648 * 649 * @param {boolean} extscript - Whether external script exists 650 * @param {boolean} extcss - Whether external CSS exists 651 * @returns {string} Generated initialization code 652 */ 653function loadPageCheckLite (extscript, extcss) { 654 return (extscript ? `var options=$app_script$\n if ($app_script$.__esModule) {\n 655 options = $app_script$.default;\n }\n` : `var options={}\n`) + 656 (extcss ? `options.styleSheet=$app_style$\n` : ``) + 657 `options.render=$app_template$;\nmodule.exports=new ViewModel(options);` 658} 659 660for (const key in legacy) { 661 loader[key] = legacy[key] 662} 663 664/** 665 * Checks if the given file path is an entry file and issues a warning if true 666 * 667 * @param {Object} _this - Webpack compilation context object containing build information 668 * @param {string} filePath - Absolute file path to check 669 * @param {string} elementSrc - Page path from config file, used for warning message 670 */ 671function checkEntry(_this, filePath, elementSrc) { 672 if (_this._compilation.entries) { 673 for (var key of _this._compilation.entries.keys()) { 674 const entryPath = path.join(path.resolve(process.env.projectPath), key + '.hml'); 675 if (entryPath === filePath) { 676 logWarn(_this, [{ 677 reason: `WARNING: The page "${elementSrc}" configured in 'config.json'` + 678 ` can not be uesd as a custom component.` + 679 `To ensure that the debugging function is normal, please delete this page in 'config.json'.` 680 }]); 681 } 682 } 683 } 684} 685 686module.exports = loader