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