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 54function getLoaderString (type, config) { 55 config = config || {} 56 const customLoader = loadCustomLoader(config) 57 let loaders 58 switch (type) { 59 case 'main': 60 return mainLoaderString(loaders) 61 case 'element': 62 return elementLoaderString(loaders, config) 63 case 'template': 64 return templateLoaderString(loaders, config, customLoader) 65 case 'style': 66 return styleLoaderString(loaders, config, customLoader) 67 case 'script': 68 return scriptLoaderString(loaders, config, customLoader) 69 case 'config': 70 return configLoaderString(loaders, config) 71 case 'data': 72 return dataLoaderString(loaders, config) 73 } 74} 75 76function loadCustomLoader (config) { 77 if (config.lang && config.customLang[config.lang]) { 78 return loadBabelModule(config.customLang[config.lang][0]) 79 } 80} 81 82function mainLoaderString (loaders) { 83 loaders = [{ 84 name: defaultLoaders.main 85 }] 86 return stringifyLoaders(loaders) 87} 88 89function elementLoaderString (loaders, config) { 90 loaders = [{ 91 name: defaultLoaders.main, 92 query: { 93 element: config.source ? undefined : true 94 } 95 }] 96 return stringifyLoaders(loaders) 97} 98 99function templateLoaderString (loaders, config, customLoader) { 100 loaders = [{ 101 name: defaultLoaders.json 102 }, { 103 name: defaultLoaders.template 104 }] 105 if (customLoader) { 106 loaders = loaders.concat(customLoader) 107 } 108 return stringifyLoaders(loaders) 109} 110 111function styleLoaderString (loaders, config, customLoader) { 112 loaders = [{ 113 name: defaultLoaders.json 114 }, { 115 name: defaultLoaders.style 116 }] 117 if (customLoader) { 118 loaders = loaders.concat(customLoader) 119 } 120 return stringifyLoaders(loaders) 121} 122 123function scriptLoaderString (loaders, config, customLoader) { 124 loaders = [{ 125 name: defaultLoaders.script 126 }] 127 if (customLoader) { 128 loaders = loaders.concat(customLoader) 129 } 130 else { 131 const isTargets = { 132 'extends': path.resolve(__dirname, "../babel.config.js") 133 } 134 if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH) { 135 isTargets['targets'] = 'node 8'; 136 } 137 loaders.push({ 138 name: defaultLoaders.babel, 139 query: isTargets, 140 }) 141 loaders.push({ 142 name: defaultLoaders.resourceReferenceScript 143 }) 144 } 145 if (config.app && process.env.abilityType === 'page' && 146 fs.existsSync(process.env.aceManifestPath)) { 147 loaders.push({ 148 name: defaultLoaders.manifest, 149 query: { 150 path: config.source 151 } 152 }) 153 } 154 return stringifyLoaders(loaders) 155} 156 157function configLoaderString (loaders, config) { 158 loaders = [{ 159 name: defaultLoaders.json 160 }] 161 return stringifyLoaders(loaders) 162} 163 164function dataLoaderString (loaders, config) { 165 loaders = [{ 166 name: defaultLoaders.json 167 }] 168 return stringifyLoaders(loaders) 169} 170 171function loader (source) { 172 this.cacheable && this.cacheable() 173 174 const options = { 175 lang: { 176 sass:['sass-loader'], 177 scss:['sass-loader'], 178 less:['less-loader'] 179 } 180 } 181 const customLang = options.lang || {} 182 const resourceQuery = this.resourceQuery && loaderUtils.parseQuery(this.resourceQuery) || {} 183 const isEntry = resourceQuery.entry 184 const dirName = path.parse(this.resourcePath) 185 const name = isEntry ? dirName.name : resourceQuery.name || getNameByPath(this.resourcePath) 186 let parentPath = resourceQuery.parentPath || this.resourcePath; 187 if (isEntry) { 188 elements[this.resourcePath] = elements[this.resourcePath] || {}; 189 elements[this.resourcePath][name] = true; 190 } else { 191 elements[this.resourcePath] = elements[this.resourcePath] || {}; 192 elements[this.resourcePath]["parent"] = parentPath; 193 if (elements[parentPath] && elements[parentPath]["parent"]) { 194 elements[this.resourcePath]["parent"] = elements[elements[parentPath]["parent"]]; 195 parentPath = elements[this.resourcePath]["parent"]; 196 } 197 } 198 if (isReservedTag(name) && process.env.abilityType === 'page') { 199 logWarn(this, [{ 200 reason: 'ERROR: The file name cannot contain reserved tag name: ' + name 201 }]) 202 return '' 203 } 204 let output = '' 205 // import app.js 206 output += loadApp(this, name, isEntry, customLang, source) 207 output += loadPage(this, name, isEntry, customLang, source, parentPath); 208 return output 209} 210 211function checkApp(_this) { 212 if (process.env.abilityType === 'testrunner') { 213 return true; 214 } 215 return _this.resourcePath === path.resolve(process.env.projectPath, 216 process.env.abilityType === 'page' ? 'app.js' : `${process.env.abilityType}.js`) 217} 218 219function loadApp (_this, name, isEntry, customLang, source) { 220 let output = '' 221 let extcss = false 222 if (checkApp(_this)) { 223 const filename = _this.resourcePath.replace(path.extname(_this.resourcePath).toString(), '') 224 // find css 225 const cssFileName = filename + '.css' 226 if (!fs.existsSync(cssFileName)) { 227 extcss = false 228 } 229 else { 230 extcss = true 231 output += 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', { 232 customLang, 233 lang: undefined, 234 element: undefined, 235 elementName: undefined, 236 source: cssFileName 237 }), cssFileName) 238 } 239 output += 'var $app_script$ = ' + getRequireString(_this, getLoaderString('script', { 240 customLang, 241 lang: undefined, 242 element: undefined, 243 elementName: undefined, 244 source: _this.resourcePath, 245 app: true 246 }), _this.resourcePath) 247 248 if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH || process.env.DEVICE_LEVEL === 'card') { 249 output += ` 250 $app_define$('@app-application/${name}', [], function($app_require$, $app_exports$, $app_module$) { 251 ` + ` 252 $app_script$($app_module$, $app_exports$, $app_require$) 253 if ($app_exports$.__esModule && $app_exports$.default) { 254 $app_module$.exports = $app_exports$.default 255 } 256 ` + (extcss ? ` 257 $app_module$.exports.style = $app_style$ 258 ` : '') 259 + ` 260 }) 261 ` 262 if (isEntry) { 263 output += `$app_bootstrap$('@app-application/${name}'` + ',undefined' + ',undefined' + `)` 264 } 265 } 266 if (process.env.DEVICE_LEVEL === DEVICE_LEVEL.LITE) { 267 output += `var options=$app_script$\n if ($app_script$.__esModule) {\n 268 options = $app_script$.default;\n }\n` + 269 (extcss ? `options.styleSheet=$app_style$\n` : ``) + 270 `module.exports=new ViewModel(options);` 271 } 272 return output 273 } else if (/\.js$/.test(_this.resourcePath)) { 274 return source 275 } else { 276 return output 277 } 278} 279 280function loadPage (_this, name, isEntry, customLang, source, parentPath) { 281 let output = '' 282 if (path.extname(_this.resourcePath).match(/\.hml/)) { 283 const filename = _this.resourcePath.replace(path.extname(_this.resourcePath).toString(), '') 284 const resourcePath = _this.resourcePath 285 const loaderQuery = loaderUtils.getOptions(_this) || {} 286 const isElement = loaderQuery.element 287 const frag = parseFragment(source) 288 const elementNames = [] 289 const elementLength = frag.element.length 290 output += loadPageCheckElementLength(_this, elementLength, frag, elementNames, resourcePath, 291 customLang, parentPath); 292 293 output += 'var $app_template$ = ' + getRequireString(_this, getLoaderString('template', { 294 customLang, 295 lang: undefined, 296 element: isElement, 297 elementName: isElement ? name : undefined, 298 source: _this.resourcePath 299 }), _this.resourcePath) 300 301 // find css 302 const cssContent = loadPageFindCss(_this, filename, customLang) 303 const extcss = cssContent.extcss 304 output += cssContent.output 305 306 // find js 307 const scriptContent = loadPageFindJs(_this, filename, customLang) 308 const extscript = scriptContent.extscript 309 output += scriptContent.output 310 311 output += process.env.DEVICE_LEVEL === DEVICE_LEVEL.RICH ? loadPageCheckRich(name, extscript, extcss, isEntry) : 312 loadPageCheckLite(extscript, extcss) 313 return output 314 } 315 return output 316} 317 318function loadPageCheckElementLength (_this, elementLength, frag, elementNames, resourcePath, 319 customLang, parentPath) { 320 let output = '' 321 if (elementLength) { 322 for (let i = 0; i < elementLength; i++) { 323 const element = frag.element[i] 324 let src = resourcePath 325 if (element.src) { 326 src = element.src 327 if (!src.match(/\.hml$/)) { 328 src = src.concat('.hml') 329 } 330 const filePath = path.join(path.dirname(resourcePath), src) 331 if (!fs.existsSync(filePath) && src.match(/^(\/|\.)/)) { 332 logWarn(_this, [{ 333 reason: 'ERROR: The file path of custom element does not exist, src: ' + src 334 }]) 335 return '' 336 } 337 if (!element.name) { 338 element.name = path.parse(src).name 339 } 340 element.name = element.name.toLowerCase(); 341 elements[parentPath] = elements[parentPath] || {}; 342 if (elements[parentPath][element.name]) { 343 logWarn(_this, [{ 344 reason: `ERROR: The element name can not be same with the page ` + 345 `"${element.name}" (ignore case).` 346 }]); 347 } else { 348 elements[parentPath][element.name] = true; 349 } 350 checkEntry(_this, filePath, element.src) 351 } 352 else { 353 logWarn(_this, [{ 354 reason: 'ERROR: src attributes must be set for custom elements.' 355 }]) 356 return '' 357 } 358 elementNames.push(element.name) 359 output += getRequireString(_this, getLoaderString('element', { 360 customLang, 361 name: element.name, 362 source: src 363 }), `${src}?name=${element.name}&parentPath=${parentPath}`) 364 } 365 } 366 return output 367} 368 369function loadPageFindCss (_this, filename, customLang) { 370 let output = '' 371 let extcss = false 372 const cssFileName = filename + '.css' 373 if (fs.existsSync(cssFileName)) { 374 extcss = true 375 output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', { 376 customLang, 377 lang: undefined, 378 element: undefined, 379 elementName: undefined, 380 source: cssFileName 381 }), cssFileName) 382 } 383 else { 384 // find less 385 const lessFileName = filename + '.less' 386 if (fs.existsSync(lessFileName)) { 387 extcss = true 388 output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', { 389 customLang, 390 lang: 'less', 391 element: undefined, 392 elementName: undefined, 393 source: lessFileName 394 }), lessFileName) 395 } 396 else { 397 // find scss 398 const scssFileName = filename + '.scss' 399 if (fs.existsSync(scssFileName)) { 400 extcss = true 401 output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', { 402 customLang, 403 lang: 'scss', 404 element: undefined, 405 elementName: undefined, 406 source: scssFileName 407 }), scssFileName) 408 } 409 else { 410 // find sass 411 const sassFileName = filename + '.sass' 412 if (fs.existsSync(sassFileName)) { 413 extcss = true 414 output = 'var $app_style$ = ' + getRequireString(_this, getLoaderString('style', { 415 customLang, 416 lang: 'sass', 417 element: undefined, 418 elementName: undefined, 419 source: sassFileName 420 }), sassFileName) 421 } 422 else { 423 extcss = false 424 } 425 } 426 } 427 } 428 return { 429 extcss: extcss, 430 output: output 431 } 432} 433 434function loadPageFindJs (_this, filename, customLang) { 435 let output = '' 436 let extscript = false 437 const jsFileName = filename + '.js' 438 if (!fs.existsSync(jsFileName)) { 439 extscript = false 440 console.log('missing ' + jsFileName) 441 } 442 else { 443 extscript = true 444 output = 'var $app_script$ = ' + getRequireString(_this, getLoaderString('script', { 445 customLang, 446 lang: undefined, 447 element: undefined, 448 elementName: undefined, 449 source: jsFileName 450 }), jsFileName) 451 } 452 return { 453 extscript: extscript, 454 output: output 455 } 456} 457 458function loadPageCheckRich (name, extscript, extcss, isEntry) { 459 let output = '' 460 output += ` 461$app_define$('@app-component/${name}', [], function($app_require$, $app_exports$, $app_module$) { 462` + (extscript ? ` 463$app_script$($app_module$, $app_exports$, $app_require$) 464if ($app_exports$.__esModule && $app_exports$.default) { 465$app_module$.exports = $app_exports$.default 466} 467` : '') + ` 468$app_module$.exports.template = $app_template$ 469` + (extcss ? ` 470$app_module$.exports.style = $app_style$ 471` : '') + ` 472}) 473` 474 if (isEntry) { 475 output += `$app_bootstrap$('@app-component/${name}'` + ',undefined' + ',undefined' + `)` 476 } 477 return output 478} 479 480function loadPageCheckLite (extscript, extcss) { 481 return (extscript ? `var options=$app_script$\n if ($app_script$.__esModule) {\n 482 options = $app_script$.default;\n }\n` : `var options={}\n`) + 483 (extcss ? `options.styleSheet=$app_style$\n` : ``) + 484 `options.render=$app_template$;\nmodule.exports=new ViewModel(options);` 485} 486 487for (const key in legacy) { 488 loader[key] = legacy[key] 489} 490 491function checkEntry(_this, filePath, elementSrc) { 492 if (_this._compilation.entries) { 493 for (var key of _this._compilation.entries.keys()) { 494 const entryPath = path.join(path.resolve(process.env.projectPath), key + '.hml'); 495 if (entryPath === filePath) { 496 logWarn(_this, [{ 497 reason: `WARNING: The page "${elementSrc}" configured in 'config.json'` + 498 ` can not be uesd as a custom component.` + 499 `To ensure that the debugging function is normal, please delete this page in 'config.json'.` 500 }]); 501 } 502 } 503 } 504} 505 506module.exports = loader