* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import loaderUtils from 'loader-utils'
import path from 'path'
import fs from 'fs'
import * as legacy from './legacy'
import {
parseFragment
}
from './parser'
import {
getNameByPath,
getRequireString,
stringifyLoaders,
logWarn,
loadBabelModule,
elements
}
from './util'
import { isReservedTag } from './templater/component_validator'
const { DEVICE_LEVEL } = require('./lite/lite-enum')
const loaderPath = __dirname
const defaultLoaders = {
none: '',
main: path.resolve(loaderPath, 'loader.js'),
template: path.resolve(loaderPath, 'template.js'),
style: path.resolve(loaderPath, 'style.js'),
script: path.resolve(loaderPath, 'script.js'),
json: path.resolve(loaderPath, 'json.js'),
babel: loadBabelModule('babel-loader'),
manifest: path.resolve(loaderPath, 'manifest-loader.js'),
resourceReferenceScript: path.resolve(loaderPath, 'resource-reference-script.js')
}
* Central loader configuration factory that returns the appropriate loader string
* based on file type and configuration. Acts as a dispatcher for specialized loader strings.
*
* @param {string} type - The type of loader needed (main/element/template/style/script/config/data)
* @param {Object} config - Configuration options for the loader including:
* - lang: Language specification
* - customLang: Custom language loaders
* - source: Source file path
* - app: Boolean flag for application scripts
* @returns {string} Webpack-compatible loader string
*/
function getLoaderString (type, config) {
config = config || {}
const customLoader = loadCustomLoader(config)
let loaders
switch (type) {
case 'main':
return mainLoaderString(loaders)
case 'element':
return elementLoaderString(loaders, config)
case 'template':
return templateLoaderString(loaders, config, customLoader)
case 'style':
return styleLoaderString(loaders, config, customLoader)
case 'script':
return scriptLoaderString(loaders, config, customLoader)
case 'config':
return configLoaderString(loaders, config)
case 'data':
return dataLoaderString(loaders, config)
}
}
* Loads a custom language loader based on configuration
* Primarily used for loading Babel-compatible language processors (like TypeScript, Flow, etc.)
*
* @param {Object} config - Configuration object containing:
* - lang: The language to load (e.g., 'typescript', 'flow')
* - customLang: Map of available custom languages and their loaders
* @returns {Function|undefined} The loaded custom loader function, or undefined if not found
*/
function loadCustomLoader (config) {
if (config.lang && config.customLang[config.lang]) {
return loadBabelModule(config.customLang[config.lang][0])
}
}
* Generates a webpack loader string using the default main loader
* Provides a clean way to get the standard main loader configuration
*
* @param {Array} loaders - Initial loader array (will be overridden)
* @returns {string} Webpack-compatible loader string with just the main loader
*/
function mainLoaderString (loaders) {
loaders = [{
name: defaultLoaders.main
}]
return stringifyLoaders(loaders)
}
* Generates a webpack loader string for processing custom elements/components
* Configures the main loader with element-specific options
*
* @param {Array} loaders - Initial loader array (will be overridden)
* @param {Object} config - Configuration options including:
* - source: Source file path (optional)
* @returns {string} Webpack-compatible loader string
*/
function elementLoaderString (loaders, config) {
loaders = [{
name: defaultLoaders.main,
query: {
element: config.source ? undefined : true
}
}]
return stringifyLoaders(loaders)
}
* Generates a webpack loader string for processing template files
* Combines default JSON parsing with template processing, plus optional custom loaders
*
* @param {Array} loaders - Initial loader array (will be overridden)
* @param {Object} config - Configuration options (currently unused)
* @param {Array} customLoader - Optional custom loaders to append
* @returns {string} Webpack-compatible loader string
*/
function templateLoaderString (loaders, config, customLoader) {
loaders = [{
name: defaultLoaders.json
}, {
name: defaultLoaders.template
}]
if (customLoader) {
loaders = loaders.concat(customLoader)
}
return stringifyLoaders(loaders)
}
* Generates a webpack loader string for processing style files (CSS/LESS/SASS/etc)
* Combines default JSON and style loaders with optional custom loaders
*
* @param {Array} loaders - Initial loader configuration (will be overridden)
* @param {Object} config - Configuration options (unused in current implementation)
* @param {Array} customLoader - Optional array of custom loaders to append
* @returns {string} Webpack-compatible loader string
*/
function styleLoaderString (loaders, config, customLoader) {
loaders = [{
name: defaultLoaders.json
}, {
name: defaultLoaders.style
}]
if (customLoader) {
loaders = loaders.concat(customLoader)
}
return stringifyLoaders(loaders)
}
* Generates a webpack loader string for JavaScript/script files with configurable options
* Supports custom loaders, Babel transpilation, and manifest processing for apps
*
* @param {Array} loaders - Initial loader configuration (will be overridden)
* @param {Object} config - Configuration options including:
* - app: boolean indicating if processing app script
* - source: source file path
* @param {Array} customLoader - Optional custom loaders to include
* @returns {string} Webpack-compatible loader string
*/
function scriptLoaderString (loaders, config, customLoader) {
loaders = [{
name: defaultLoaders.script
}]
if (customLoader) {
loaders = loaders.concat(customLoader)
}
else {
const isTargets = {
'extends': path.resolve(__dirname, "../babel.config.js")
}
if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH) {
isTargets['targets'] = 'node 8';
}
loaders.push({
name: defaultLoaders.babel,
query: isTargets,
})
loaders.push({
name: defaultLoaders.resourceReferenceScript
})
}
if (config.app && process.env.abilityType === 'page' &&
fs.existsSync(process.env.aceManifestPath)) {
loaders.push({
name: defaultLoaders.manifest,
query: {
path: config.source
}
})
}
return stringifyLoaders(loaders)
}
* Generates a webpack loader string specifically for configuration files (e.g., JSON configs)
* Defaults to using the standard JSON loader regardless of input
*
* @param {Array} loaders - Original loader array (gets overridden)
* @param {Object} config - Additional configuration options (currently unused)
* @returns {string} Webpack-compatible loader string
*/
function configLoaderString (loaders, config) {
loaders = [{
name: defaultLoaders.json
}]
return stringifyLoaders(loaders)
}
* Generates a loader string for processing data files (e.g., JSON)
* Uses default JSON loader configuration and stringifies the loader chain
*
* @param {Array} loaders - Original loader configuration (overridden in this function)
* @param {Object} config - Additional configuration object (unused in current implementation)
* @returns {string} Stringified loader configuration
*/
function dataLoaderString (loaders, config) {
loaders = [{
name: defaultLoaders.json
}]
return stringifyLoaders(loaders)
}
* Webpack loader function that processes application and page components
* Handles both entry files and child components with proper dependency tracking
*
* @param {string} source - The source code of the file being processed
* @returns {string} The processed output code
*/
function loader (source) {
this.cacheable && this.cacheable()
const options = {
lang: {
sass:['sass-loader'],
scss:['sass-loader'],
less:['less-loader']
}
}
const customLang = options.lang || {}
const resourceQuery = this.resourceQuery && loaderUtils.parseQuery(this.resourceQuery) || {}
const isEntry = resourceQuery.entry
const dirName = path.parse(this.resourcePath)
const name = isEntry ? dirName.name : resourceQuery.name || getNameByPath(this.resourcePath)
let parentPath = resourceQuery.parentPath || this.resourcePath;
if (isEntry) {
elements[this.resourcePath] = elements[this.resourcePath] || {};
elements[this.resourcePath][name] = true;
} else {
elements[this.resourcePath] = elements[this.resourcePath] || {};
elements[this.resourcePath]["parent"] = parentPath;
if (elements[parentPath] && elements[parentPath]["parent"]) {
elements[this.resourcePath]["parent"] = elements[elements[parentPath]["parent"]];
parentPath = elements[this.resourcePath]["parent"];
}
}
if (isReservedTag(name) && process.env.abilityType === 'page') {
logWarn(this, [{
reason: 'ERROR: The file name cannot contain reserved tag name: ' + name
}])
return ''
}
let output = ''
output += loadApp(this, name, isEntry, customLang, source)
output += loadPage(this, name, isEntry, customLang, source, parentPath);
return output
}
* Determines if the current resource is the main application file
*
* @param {Object} _this - Webpack loader context
* @returns {boolean} True if the file is the main application file, false otherwise
*/
function checkApp(_this) {
if (process.env.abilityType === 'testrunner') {
return true;
}
return _this.resourcePath === path.resolve(process.env.projectPath,
process.env.abilityType === 'page' ? 'app.js' : `${process.env.abilityType}.js`)
}
* Loads and processes an application entry file (app.js)
* Handles both Rich and Lite device levels, including CSS and script loading
*
* @param {Object} _this - Webpack loader context
* @param {string} name - Application name
* @param {boolean} isEntry - Whether this is an entry point
* @param {string} customLang - Custom language setting
* @param {string} source - Source content of the file
* @returns {string} Generated output code for the application
*/
function loadApp (_this, name, isEntry, customLang, source) {
let output = ''
let extcss = false
if (checkApp(_this)) {
const filename = _this.resourcePath.replace(path.extname(_this.resourcePath).toString(), '')
const cssFileName = filename + '.css'
if (!fs.existsSync(cssFileName)) {
extcss = false
}
else {
extcss = true
output += 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', {
customLang,
lang: undefined,
element: undefined,
elementName: undefined,
source: cssFileName
}), cssFileName)
}
output += 'var $app_script$ = ' + getRequireString(_this, getLoaderString('script', {
customLang,
lang: undefined,
element: undefined,
elementName: undefined,
source: _this.resourcePath,
app: true
}), _this.resourcePath)
if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH || process.env.DEVICE_LEVEL === 'card') {
output += `
$app_define$('@app-application/${name}', [], function($app_require$, $app_exports$, $app_module$) {
` + `
$app_script$($app_module$, $app_exports$, $app_require$)
if ($app_exports$.__esModule && $app_exports$.default) {
$app_module$.exports = $app_exports$.default
}
` + (extcss ? `
$app_module$.exports.style = $app_style$
` : '')
+ `
})
`
if (isEntry) {
output += `$app_bootstrap$('@app-application/${name}'` + ',undefined' + ',undefined' + `)`
}
}
if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.LITE) {
output += `var options=$app_script$\n if ($app_script$.__esModule) {\n
options = $app_script$.default;\n }\n` +
(extcss ? `options.styleSheet=$app_style$\n` : ``) +
`module.exports=new ViewModel(options);`
}
return output
} else if (/\.js$/.test(_this.resourcePath)) {
return source
} else {
return output
}
}
* Main function for loading and processing a page/component
* Coordinates the loading of all associated resources (template, CSS, JS)
* and generates the final output code for either Rich or Lite device level
*
* @param {Object} _this - Webpack loader context
* @param {string} name - Name of the component/page
* @param {boolean} isEntry - Whether this is an entry point
* @param {string} customLang - Custom language setting
* @param {string} source - Source content of the file
* @param {string} parentPath - Path of the parent component
* @returns {string} Generated output code for the component
*/
function loadPage (_this, name, isEntry, customLang, source, parentPath) {
let output = ''
if (path.extname(_this.resourcePath).match(/\.hml/)) {
const filename = _this.resourcePath.replace(path.extname(_this.resourcePath).toString(), '')
const resourcePath = _this.resourcePath
const loaderQuery = loaderUtils.getOptions(_this) || {}
const isElement = loaderQuery.element
const frag = parseFragment(source)
const elementNames = []
const elementLength = frag.element.length
output += loadPageCheckElementLength(_this, elementLength, frag, elementNames, resourcePath,
customLang, parentPath);
output += 'var $app_template$ = ' + getRequireString(_this, getLoaderString('template', {
customLang,
lang: undefined,
element: isElement,
elementName: isElement ? name : undefined,
source: _this.resourcePath
}), _this.resourcePath)
const cssContent = loadPageFindCss(_this, filename, customLang)
const extcss = cssContent.extcss
output += cssContent.output
const scriptContent = loadPageFindJs(_this, filename, customLang)
const extscript = scriptContent.extscript
output += scriptContent.output
output += process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH ? loadPageCheckRich(name, extscript, extcss, isEntry) :
loadPageCheckLite(extscript, extcss)
return output
}
return output
}
* Processes custom elements in a template and generates corresponding require statements
* Validates element configurations and checks for naming conflicts
*
* @param {Object} _this - Webpack compilation context
* @param {number} elementLength - Number of custom elements to process
* @param {Object} frag - Fragment containing element definitions
* @param {Array} elementNames - Array to collect processed element names
* @param {string} resourcePath - Path of the parent resource
* @param {string} customLang - Custom language setting for loaders
* @param {string} parentPath - Path of the parent component
* @returns {string} Generated require statements for all valid elements
*/
function loadPageCheckElementLength (_this, elementLength, frag, elementNames, resourcePath,
customLang, parentPath) {
let output = ''
if (elementLength) {
for (let i = 0; i < elementLength; i++) {
const element = frag.element[i]
let src = resourcePath
if (element.src) {
src = element.src
if (!src.match(/\.hml$/)) {
src = src.concat('.hml')
}
const filePath = path.join(path.dirname(resourcePath), src)
if (!fs.existsSync(filePath) && src.match(/^(\/|\.)/)) {
logWarn(_this, [{
reason: 'ERROR: The file path of custom element does not exist, src: ' + src
}])
return ''
}
if (!element.name) {
element.name = path.parse(src).name
}
element.name = element.name.toLowerCase();
elements[parentPath] = elements[parentPath] || {};
if (elements[parentPath][element.name]) {
logWarn(_this, [{
reason: `ERROR: The element name can not be same with the page ` +
`"${element.name}" (ignore case).`
}]);
} else {
elements[parentPath][element.name] = true;
}
checkEntry(_this, filePath, element.src)
}
else {
logWarn(_this, [{
reason: 'ERROR: src attributes must be set for custom elements.'
}])
return ''
}
elementNames.push(element.name)
output += getRequireString(_this, getLoaderString('element', {
customLang,
name: element.name,
source: src
}), `${src}?name=${element.name}&parentPath=${parentPath}`)
}
}
return output
}
* Checks for and loads CSS or preprocessor style files associated with a component/page
* Supports CSS, LESS, SCSS, and SASS file formats
* Generates the require statement for the style file if found
*
* @param {Object} _this - Webpack compilation context
* @param {string} filename - Base filename (without extension)
* @param {string} customLang - Custom language setting for the loader
* @returns {Object} Returns an object containing:
* - extcss: boolean indicating if any style file exists
* - output: generated require statement or empty string
*/
function loadPageFindCss (_this, filename, customLang) {
let output = ''
let extcss = false
const cssFileName = filename + '.css'
if (fs.existsSync(cssFileName)) {
extcss = true
output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', {
customLang,
lang: undefined,
element: undefined,
elementName: undefined,
source: cssFileName
}), cssFileName)
}
else {
const lessFileName = filename + '.less'
if (fs.existsSync(lessFileName)) {
extcss = true
output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', {
customLang,
lang: 'less',
element: undefined,
elementName: undefined,
source: lessFileName
}), lessFileName)
}
else {
const scssFileName = filename + '.scss'
if (fs.existsSync(scssFileName)) {
extcss = true
output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', {
customLang,
lang: 'scss',
element: undefined,
elementName: undefined,
source: scssFileName
}), scssFileName)
}
else {
const sassFileName = filename + '.sass'
if (fs.existsSync(sassFileName)) {
extcss = true
output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', {
customLang,
lang: 'sass',
element: undefined,
elementName: undefined,
source: sassFileName
}), sassFileName)
}
else {
extcss = false
}
}
}
}
return {
extcss: extcss,
output: output
}
}
* Checks for and loads a JavaScript file associated with a component/page
* Generates the require statement for the JS file if it exists
*
* @param {Object} _this - Webpack compilation context
* @param {string} filename - Base filename (without extension)
* @param {string} customLang - Custom language setting for the loader
* @returns {Object} Returns an object containing:
* - extscript: boolean indicating if JS file exists
* - output: generated require statement or empty string
*/
function loadPageFindJs (_this, filename, customLang) {
let output = ''
let extscript = false
const jsFileName = filename + '.js'
if (!fs.existsSync(jsFileName)) {
extscript = false
console.log('missing ' + jsFileName)
}
else {
extscript = true
output = 'var $app_script$ = ' + getRequireString(_this, getLoaderString('script', {
customLang,
lang: undefined,
element: undefined,
elementName: undefined,
source: jsFileName
}), jsFileName)
}
return {
extscript: extscript,
output: output
}
}
* Generates component initialization code for Rich mode
* Creates a component definition with optional script and style, and handles entry point bootstrapping
*
* @param {string} name - Component name
* @param {boolean} extscript - Whether the component has an external script
* @param {boolean} extcss - Whether the component has external CSS
* @param {boolean} isEntry - Whether this is an entry component
* @returns {string} Generated component definition and bootstrap code
*/
function loadPageCheckRich (name, extscript, extcss, isEntry) {
let output = ''
output += `
$app_define$('@app-component/${name}', [], function($app_require$, $app_exports$, $app_module$) {
` + (extscript ? `
$app_script$($app_module$, $app_exports$, $app_require$)
if ($app_exports$.__esModule && $app_exports$.default) {
$app_module$.exports = $app_exports$.default
}
` : '') + `
$app_module$.exports.template = $app_template$
` + (extcss ? `
$app_module$.exports.style = $app_style$
` : '') + `
})
`
if (isEntry) {
output += `$app_bootstrap$('@app-component/${name}'` + ',undefined' + ',undefined' + `)`
}
return output
}
* Generates the page initialization code for Lite mode
* Combines script, style, and template components into a ViewModel instance
*
* @param {boolean} extscript - Whether external script exists
* @param {boolean} extcss - Whether external CSS exists
* @returns {string} Generated initialization code
*/
function loadPageCheckLite (extscript, extcss) {
return (extscript ? `var options=$app_script$\n if ($app_script$.__esModule) {\n
options = $app_script$.default;\n }\n` : `var options={}\n`) +
(extcss ? `options.styleSheet=$app_style$\n` : ``) +
`options.render=$app_template$;\nmodule.exports=new ViewModel(options);`
}
for (const key in legacy) {
loader[key] = legacy[key]
}
* Checks if the given file path is an entry file and issues a warning if true
*
* @param {Object} _this - Webpack compilation context object containing build information
* @param {string} filePath - Absolute file path to check
* @param {string} elementSrc - Page path from config file, used for warning message
*/
function checkEntry(_this, filePath, elementSrc) {
if (_this._compilation.entries) {
for (var key of _this._compilation.entries.keys()) {
const entryPath = path.join(path.resolve(process.env.projectPath), key + '.hml');
if (entryPath === filePath) {
logWarn(_this, [{
reason: `WARNING: The page "${elementSrc}" configured in 'config.json'` +
` can not be uesd as a custom component.` +
`To ensure that the debugging function is normal, please delete this page in 'config.json'.`
}]);
}
}
}
}
module.exports = loader