• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2021 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16const fs = require('fs');
17const path = require('path');
18const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
19
20const CUSTOM_THEME_PROP_GROUPS = require('./theme/customThemeStyles');
21const OHOS_THEME_PROP_GROUPS = require('./theme/ohosStyles');
22import { mkDir } from './util';
23import { multiResourceBuild } from '../main.product';
24
25const FILE_EXT_NAME = ['.js', '.css', '.jsx', '.less', '.sass', '.scss', '.md', '.DS_Store', '.hml', '.json'];
26const red = '\u001b[31m';
27const reset = '\u001b[39m';
28let input = '';
29let output = '';
30let manifestFilePath = '';
31let watchCSSFiles;
32let shareThemePath = '';
33let internalThemePath = '';
34let resourcesPath;
35let sharePath;
36let workerFile;
37
38function copyFile(input, output) {
39  try {
40    if (fs.existsSync(input)) {
41      const parent = path.join(output, '..');
42      if (!(fs.existsSync(parent) && fs.statSync(parent).isDirectory())) {
43        mkDir(parent);
44      }
45      const pathInfo = path.parse(input);
46      const entryObj = addPageEntryObj();
47      const indexPath = pathInfo.dir + path.sep + pathInfo.name + '.hml?entry';
48      for (const key in entryObj) {
49        if (entryObj[key] === indexPath) {
50          return;
51        }
52      }
53      if (pathInfo.ext === '.json' && (pathInfo.dir === shareThemePath ||
54        pathInfo.dir === internalThemePath)) {
55        if (themeFileBuild(input, output)) {
56          return;
57        }
58      }
59      const readStream = fs.createReadStream(input);
60      const writeStream = fs.createWriteStream(output);
61      readStream.pipe(writeStream);
62      readStream.on('close', function() {
63        writeStream.end();
64      });
65    }
66  } catch (e) {
67    if (e && /ERROR: /.test(e)) {
68      throw e;
69    } else {
70      throw new Error(`${red}Failed to build file ${input}.${reset}`).message;
71    }
72  }
73}
74
75function circularFile(inputPath, outputPath, ext) {
76  const realPath = path.join(inputPath, ext);
77  if (!fs.existsSync(realPath) || realPath === output) {
78    return;
79  }
80  fs.readdirSync(realPath).forEach(function(file_) {
81    const file = path.join(realPath, file_);
82    const fileStat = fs.statSync(file);
83    if (fileStat.isFile()) {
84      const baseName = path.basename(file);
85      const extName = path.extname(file);
86      const outputFile = path.join(outputPath, ext, path.basename(file_));
87      if (outputFile === path.join(output, 'manifest.json')) {
88        return;
89      }
90      if (FILE_EXT_NAME.indexOf(extName) < 0 && baseName !== '.DS_Store') {
91        toCopyFile(file, outputFile, fileStat)
92      } else if (extName === '.json') {
93        const parent = path.join(file, '..');
94        const parent_ = path.join(parent, '..');
95        if (path.parse(parent).name === 'i18n' && path.parse(parent_).name === 'share') {
96          toCopyFile(file, outputFile, fileStat)
97        } else if (file.toString().startsWith(resourcesPath) || file.toString().startsWith(sharePath)) {
98          toCopyFile(file, outputFile, fileStat)
99        }
100      }
101    } else if (fileStat.isDirectory()) {
102      circularFile(inputPath, outputPath, path.join(ext, file_));
103    }
104  });
105}
106
107function toCopyFile(file, outputFile, fileStat) {
108  if (fs.existsSync(outputFile)) {
109    const outputFileStat = fs.statSync(outputFile);
110    if (outputFileStat.isFile() && fileStat.size !== outputFileStat.size) {
111      copyFile(file, outputFile);
112    }
113  } else {
114    copyFile(file, outputFile);
115  }
116}
117
118class ResourcePlugin {
119  constructor(input_, output_, manifestFilePath_, watchCSSFiles_, workerFile_ = null) {
120    input = input_;
121    output = output_;
122    manifestFilePath = manifestFilePath_;
123    watchCSSFiles = watchCSSFiles_;
124    shareThemePath = path.join(input_, '../share/resources/styles');
125    internalThemePath = path.join(input_, 'resources/styles');
126    resourcesPath = path.resolve(input_, 'resources');
127    sharePath = path.resolve(input_, '../share/resources');
128    workerFile = workerFile_;
129  }
130  apply(compiler) {
131    compiler.hooks.beforeCompile.tap('resource Copy', () => {
132      if (multiResourceBuild.value) {
133        const res = multiResourceBuild.value.res;
134        if (res) {
135          res.forEach(item => {
136            circularFile(path.resolve(input, item), path.resolve(output, item), '');
137          })
138        }
139      } else {
140        circularFile(input, output, '');
141      }
142      circularFile(input, output, '../share');
143    });
144    compiler.hooks.watchRun.tap('i18n', (comp) => {
145      checkRemove(comp);
146    });
147    compiler.hooks.normalModuleFactory.tap('OtherEntryOptionPlugin', () => {
148      if (process.env.abilityType === 'testrunner') {
149        checkTestRunner(input, entryObj);
150        for (const key in entryObj) {
151          const singleEntry = new SingleEntryPlugin('', entryObj[key], key);
152          singleEntry.apply(compiler);
153        }
154      } else {
155        const cssFiles = readCSSInfo(watchCSSFiles);
156        if (process.env.DEVICE_LEVEL === 'card' && process.env.compileMode !== 'moduleJson') {
157          for(const entryKey in compiler.options.entry) {
158            setCSSEntry(cssFiles, entryKey);
159          }
160          writeCSSInfo(watchCSSFiles, cssFiles);
161          return;
162        }
163        addPageEntryObj();
164        entryObj = Object.assign(entryObj, abilityEntryObj);
165        for (const key in entryObj) {
166          if (!compiler.options.entry[key]) {
167            const singleEntry = new SingleEntryPlugin('', entryObj[key], key);
168            singleEntry.apply(compiler);
169          }
170          setCSSEntry(cssFiles, key);
171        }
172        writeCSSInfo(watchCSSFiles, cssFiles);
173      }
174    });
175    compiler.hooks.done.tap('copyManifest', () => {
176      copyManifest();
177    });
178  }
179}
180
181function checkRemove(comp) {
182  const removedFiles = comp.removedFiles || [];
183  removedFiles.forEach(file => {
184    if (file.indexOf(process.env.projectPath) > -1 && path.extname(file) === '.json' &&
185      file.indexOf('i18n') > -1) {
186      const buildFilePath = file.replace(process.env.projectPath, process.env.buildPath);
187      if (fs.existsSync(buildFilePath)) {
188        fs.unlinkSync(buildFilePath);
189      }
190    }
191  })
192}
193
194function copyManifest() {
195  copyFile(manifestFilePath, path.join(output, 'manifest.json'));
196  if (fs.existsSync(path.join(output, 'app.js'))) {
197    fs.utimesSync(path.join(output, 'app.js'), new Date(), new Date());
198  }
199}
200
201let entryObj = {};
202let configPath;
203
204function addPageEntryObj() {
205  entryObj = {};
206  if (process.env.abilityType === 'page') {
207    let jsonContent;
208    if (multiResourceBuild.value && Object.keys(multiResourceBuild.value).length) {
209      jsonContent = multiResourceBuild.value;
210    } else {
211      jsonContent = readManifest(manifestFilePath);
212    }
213    const pages = jsonContent.pages;
214    if (pages === undefined) {
215      throw Error('ERROR: missing pages').message;
216    }
217    pages.forEach((element) => {
218      const sourcePath = element.replace(/^\.\/js\//, '');
219      const hmlPath = path.join(input, sourcePath + '.hml');
220      const aceSuperVisualPath = process.env.aceSuperVisualPath || '';
221      const visualPath = path.join(aceSuperVisualPath, sourcePath + '.visual');
222      const isHml = fs.existsSync(hmlPath);
223      const isVisual = fs.existsSync(visualPath);
224      const projectPath = process.env.projectPath;
225      if (isHml && isVisual) {
226        console.error('ERROR: ' + sourcePath + ' cannot both have hml && visual');
227      } else if (isHml) {
228        entryObj['./' + sourcePath] = path.resolve(projectPath, './' + sourcePath + '.hml?entry');
229      } else if (isVisual) {
230        entryObj['./' + sourcePath] = path.resolve(aceSuperVisualPath, './' + sourcePath +
231          '.visual?entry');
232      } else {
233        entryErrorLog(sourcePath);
234      }
235    });
236  }
237  if (process.env.isPreview !== 'true' && process.env.DEVICE_LEVEL === 'rich') {
238    loadWorker(entryObj);
239  }
240  return entryObj;
241}
242
243function entryErrorLog(sourcePath) {
244  if (process.env.watchMode && process.env.watchMode === 'true') {
245    console.error('COMPILE RESULT:FAIL ');
246    console.error('ERROR: Invalid route ' + sourcePath +
247      '. Verify the route infomation' + (configPath ?  " in the " + configPath : '') +
248      ', and then restart the Previewer.');
249    return;
250  } else {
251    throw Error(
252      '\u001b[31m' + 'ERROR: Invalid route ' + sourcePath +
253      '. Verify the route infomation' + (configPath ?  " in the " + configPath : '') +
254      ', and then restart the Build.').message;
255  }
256}
257
258let abilityEntryObj = {};
259function addAbilityEntryObj(moduleJson) {
260  abilityEntryObj = {};
261  const entranceFiles = readAbilityEntrance(moduleJson);
262  entranceFiles.forEach(filePath => {
263    const key = filePath.replace(/^\.\/js\//, './').replace(/\.js$/, '');
264    abilityEntryObj[key] = path.resolve(process.env.projectPath, '../', filePath);
265  });
266  return abilityEntryObj;
267}
268
269function readAbilityEntrance(moduleJson) {
270  const entranceFiles = [];
271  if (moduleJson.module) {
272    if (moduleJson.module.srcEntrance) {
273      entranceFiles.push(moduleJson.module.srcEntrance);
274    }
275    if (moduleJson.module.abilities && moduleJson.module.abilities.length) {
276      readEntrances(moduleJson.module.abilities, entranceFiles);
277    }
278    if (moduleJson.module.extensionAbilities && moduleJson.module.extensionAbilities.length) {
279      readEntrances(moduleJson.module.extensionAbilities, entranceFiles);
280    }
281  }
282  return entranceFiles;
283}
284
285function readEntrances(abilities, entranceFiles) {
286  abilities.forEach(ability => {
287    if ((!ability.type || ability.type !== 'form') && ability.srcEntrance) {
288      entranceFiles.push(ability.srcEntrance);
289    }
290  });
291}
292
293function readManifest(manifestFilePath) {
294  let manifest = {};
295  try {
296    if (fs.existsSync(manifestFilePath)) {
297      configPath = manifestFilePath;
298      const jsonString = fs.readFileSync(manifestFilePath).toString();
299      manifest = JSON.parse(jsonString);
300    } else if (process.env.aceModuleJsonPath && fs.existsSync(process.env.aceModuleJsonPath)) {
301      buildManifest(manifest);
302   } else {
303    throw Error('\u001b[31m' + 'ERROR: the manifest.json or module.json is lost.' +
304      '\u001b[39m').message;
305   }
306   process.env.minPlatformVersion = manifest.minPlatformVersion;
307  } catch (e) {
308    throw Error('\u001b[31m' + 'ERROR: the manifest.json or module.json file format is invalid.' +
309      '\u001b[39m').message;
310  }
311  return manifest;
312}
313
314function readModulePages(moduleJson) {
315  if (moduleJson.module.pages) {
316    const modulePagePath = path.resolve(process.env.aceProfilePath,
317      `${moduleJson.module.pages.replace(/\$profile\:/, '')}.json`);
318    if (fs.existsSync(modulePagePath)) {
319      configPath = modulePagePath;
320      const pagesConfig = JSON.parse(fs.readFileSync(modulePagePath, 'utf-8'));
321      return pagesConfig.src;
322    }
323  }
324}
325
326function readFormPages(moduleJson) {
327  const pages = [];
328  if (moduleJson.module.extensionAbilities && moduleJson.module.extensionAbilities.length) {
329    moduleJson.module.extensionAbilities.forEach(extensionAbility => {
330      if (extensionAbility.type && extensionAbility.type === 'form' && extensionAbility.metadata &&
331        extensionAbility.metadata.length) {
332        extensionAbility.metadata.forEach(item => {
333          if (item.resource && /\$profile\:/.test(item.resource)) {
334            parseFormConfig(item.resource, pages);
335          }
336        });
337      }
338    });
339  }
340  return pages;
341}
342
343function parseFormConfig(resource, pages) {
344  const resourceFile = path.resolve(process.env.aceProfilePath,
345    `${resource.replace(/\$profile\:/, '')}.json`);
346  if (fs.existsSync(resourceFile)) {
347    configPath = resourceFile;
348    const pagesConfig = JSON.parse(fs.readFileSync(resourceFile, 'utf-8'));
349    if (pagesConfig.forms && pagesConfig.forms.length) {
350      pagesConfig.forms.forEach(form => {
351        if (form.src && (form.uiSyntax === 'hml' || !form.uiSyntax)) {
352          pages.push(form.src);
353        }
354      });
355    }
356  }
357}
358
359function buildManifest(manifest) {
360  try {
361    const moduleJson =  JSON.parse(fs.readFileSync(process.env.aceModuleJsonPath).toString());
362    if (process.env.DEVICE_LEVEL === 'rich') {
363      manifest.pages = readModulePages(moduleJson);
364      manifest.abilityEntryObj = addAbilityEntryObj(moduleJson);
365    }
366    if (process.env.DEVICE_LEVEL === 'card') {
367      manifest.pages = readFormPages(moduleJson);
368    }
369    manifest.minPlatformVersion = moduleJson.app.minAPIVersion;
370  } catch (e) {
371    throw Error("\x1B[31m" + 'ERROR: the module.json file is lost or format is invalid.' +
372      "\x1B[39m").message;
373  }
374}
375
376function loadWorker(entryObj) {
377  if (workerFile) {
378    entryObj = Object.assign(entryObj, workerFile);
379  } else {
380    const workerPath = path.resolve(input, 'workers');
381    if (fs.existsSync(workerPath)) {
382      const workerFiles = [];
383      readFile(workerPath, workerFiles);
384      workerFiles.forEach((item) => {
385        if (/\.js$/.test(item)) {
386          const relativePath = path.relative(workerPath, item)
387            .replace(/\.js$/, '').replace(/\\/g, '/');
388          entryObj[`./workers/` + relativePath] = item;
389        }
390      });
391    }
392  }
393}
394
395function validateWorkOption() {
396  if (process.env.aceBuildJson && fs.existsSync(process.env.aceBuildJson)) {
397    const workerConfig = JSON.parse(fs.readFileSync(process.env.aceBuildJson).toString());
398    if(workerConfig.workers) {
399      return true;
400    }
401  }
402  return false;
403}
404
405function filterWorker(workerPath) {
406  return /\.(ts|js)$/.test(workerPath);
407}
408
409function readFile(dir, utFiles) {
410  try {
411    const files = fs.readdirSync(dir);
412    files.forEach((element) => {
413      const filePath = path.join(dir, element);
414      const status = fs.statSync(filePath);
415      if (status.isDirectory()) {
416        readFile(filePath, utFiles);
417      } else {
418        utFiles.push(filePath);
419      }
420    });
421  } catch (e) {
422    console.error(e.message);
423  }
424}
425
426function themeFileBuild(customThemePath, customThemeBuild) {
427  if (fs.existsSync(customThemePath)) {
428    const themeContent = JSON.parse(fs.readFileSync(customThemePath));
429    const newContent = {};
430    if (themeContent && themeContent['style']) {
431      newContent['style'] = {};
432      const styleContent = themeContent['style'];
433      Object.keys(styleContent).forEach(function(key) {
434        const customKey = CUSTOM_THEME_PROP_GROUPS[key];
435        const ohosKey = OHOS_THEME_PROP_GROUPS[key];
436        if (ohosKey) {
437          newContent['style'][ohosKey] = styleContent[key];
438        } else if (customKey) {
439          newContent['style'][customKey] = styleContent[key];
440        } else {
441          newContent['style'][key] = styleContent[key];
442        }
443      });
444      fs.writeFileSync(customThemeBuild, JSON.stringify(newContent, null, 2));
445      return true;
446    }
447  }
448  return false;
449}
450
451function readCSSInfo(watchCSSFiles) {
452  if (fs.existsSync(watchCSSFiles)) {
453    return JSON.parse(fs.readFileSync(watchCSSFiles));
454  } else {
455    return {};
456  }
457}
458
459function writeCSSInfo(filePath, infoObject) {
460  if (!(process.env.tddMode === 'true') && !(fs.existsSync(path.resolve(filePath, '..')) &&
461    fs.statSync(path.resolve(filePath, '..')).isDirectory())) {
462    mkDir(path.resolve(filePath, '..'));
463  }
464  if (fs.existsSync(filePath)) {
465    fs.unlinkSync(filePath);
466  }
467  if (fs.existsSync(path.resolve(filePath, '..')) && fs.statSync(path.resolve(filePath, '..')).isDirectory()) {
468    fs.writeFileSync(filePath, JSON.stringify(infoObject, null, 2));
469  }
470}
471
472function setCSSEntry(cssfiles, key) {
473  cssfiles["entry"] = cssfiles["entry"] || {};
474  if (fs.existsSync(path.join(input, key + '.css'))) {
475    cssfiles["entry"][path.join(input, key + '.css')] = true;
476  }
477}
478
479function checkTestRunner(projectPath, entryObj) {
480  const files = [];
481  walkSync(projectPath, files, entryObj, projectPath);
482}
483
484function walkSync(filePath_, files, entryObj, projectPath) {
485  fs.readdirSync(filePath_).forEach(function (name) {
486    const filePath = path.join(filePath_, name);
487    const stat = fs.statSync(filePath);
488    if (stat.isFile()) {
489      const extName = '.js';
490      if (path.extname(filePath) === extName) {
491        const key = filePath.replace(projectPath, '').replace(extName, '');
492        entryObj[`./${key}`] = filePath;
493      } else if (stat.isDirectory()) {
494        walkSync(filePath, files, entryObj, projectPath);
495      }
496    }
497  })
498}
499
500module.exports = ResourcePlugin;
501