• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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