• 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    console.error(red, `Failed to build file ${input}.`, reset);
68    throw e.message;
69  }
70}
71
72function circularFile(inputPath, outputPath, ext) {
73  const realPath = path.join(inputPath, ext);
74  if (!fs.existsSync(realPath) || realPath === output) {
75    return;
76  }
77  fs.readdirSync(realPath).forEach(function(file_) {
78    const file = path.join(realPath, file_);
79    const fileStat = fs.statSync(file);
80    if (fileStat.isFile()) {
81      const baseName = path.basename(file);
82      const extName = path.extname(file);
83      const outputFile = path.join(outputPath, ext, path.basename(file_));
84      if (outputFile === path.join(output, 'manifest.json')) {
85        return;
86      }
87      if (FILE_EXT_NAME.indexOf(extName) < 0 && baseName !== '.DS_Store') {
88        toCopyFile(file, outputFile, fileStat)
89      } else if (extName === '.json') {
90        const parent = path.join(file, '..');
91        const parent_ = path.join(parent, '..');
92        if (path.parse(parent).name === 'i18n' && path.parse(parent_).name === 'share') {
93          toCopyFile(file, outputFile, fileStat)
94        } else if (file.toString().startsWith(resourcesPath) || file.toString().startsWith(sharePath)) {
95          toCopyFile(file, outputFile, fileStat)
96        }
97      }
98    } else if (fileStat.isDirectory()) {
99      circularFile(inputPath, outputPath, path.join(ext, file_));
100    }
101  });
102}
103
104function toCopyFile(file, outputFile, fileStat) {
105  if (fs.existsSync(outputFile)) {
106    const outputFileStat = fs.statSync(outputFile);
107    if (outputFileStat.isFile() && fileStat.size !== outputFileStat.size) {
108      copyFile(file, outputFile);
109    }
110  } else {
111    copyFile(file, outputFile);
112  }
113}
114
115class ResourcePlugin {
116  constructor(input_, output_, manifestFilePath_, watchCSSFiles_, workerFile_ = null) {
117    input = input_;
118    output = output_;
119    manifestFilePath = manifestFilePath_;
120    watchCSSFiles = watchCSSFiles_;
121    shareThemePath = path.join(input_, '../share/resources/styles');
122    internalThemePath = path.join(input_, 'resources/styles');
123    resourcesPath = path.resolve(input_, 'resources');
124    sharePath = path.resolve(input_, '../share/resources');
125    workerFile = workerFile_;
126  }
127  apply(compiler) {
128    compiler.hooks.beforeCompile.tap('resource Copy', () => {
129      if (multiResourceBuild.value) {
130        const res = multiResourceBuild.value.res;
131        if (res) {
132          res.forEach(item => {
133            circularFile(path.resolve(input, item), path.resolve(output, item), '');
134          })
135        }
136      } else {
137        circularFile(input, output, '');
138      }
139      circularFile(input, output, '../share');
140    });
141    compiler.hooks.watchRun.tap('i18n', (comp) => {
142      checkRemove(comp);
143    });
144    compiler.hooks.normalModuleFactory.tap('OtherEntryOptionPlugin', () => {
145      if (process.env.abilityType === 'testrunner') {
146        checkTestRunner(input, entryObj);
147        for (const key in entryObj) {
148          const singleEntry = new SingleEntryPlugin('', entryObj[key], key);
149          singleEntry.apply(compiler);
150        }
151      } else {
152        const cssFiles = readCSSInfo(watchCSSFiles);
153        if (process.env.DEVICE_LEVEL === 'card' && process.env.compileMode !== 'moduleJson') {
154          for(const entryKey in compiler.options.entry) {
155            setCSSEntry(cssFiles, entryKey);
156          }
157          writeCSSInfo(watchCSSFiles, cssFiles);
158          return;
159        }
160        addPageEntryObj();
161        entryObj = Object.assign(entryObj, abilityEntryObj);
162        for (const key in entryObj) {
163          if (!compiler.options.entry[key]) {
164            const singleEntry = new SingleEntryPlugin('', entryObj[key], key);
165            singleEntry.apply(compiler);
166          }
167          setCSSEntry(cssFiles, key);
168        }
169        writeCSSInfo(watchCSSFiles, cssFiles);
170      }
171
172    });
173    compiler.hooks.done.tap('copyManifest', () => {
174      copyManifest();
175    });
176  }
177}
178
179function checkRemove(comp) {
180  const removedFiles = comp.removedFiles || [];
181  removedFiles.forEach(file => {
182    if (file.indexOf(process.env.projectPath) > -1 && path.extname(file) === '.json' &&
183      file.indexOf('i18n') > -1) {
184      const buildFilePath = file.replace(process.env.projectPath, process.env.buildPath);
185      if (fs.existsSync(buildFilePath)) {
186        fs.unlinkSync(buildFilePath);
187      }
188    }
189  })
190}
191
192function copyManifest() {
193  copyFile(manifestFilePath, path.join(output, 'manifest.json'));
194  if (fs.existsSync(path.join(output, 'app.js'))) {
195    fs.utimesSync(path.join(output, 'app.js'), new Date(), new Date());
196  }
197}
198
199let entryObj = {};
200
201function addPageEntryObj() {
202  entryObj = {};
203  if (process.env.abilityType === 'page') {
204    let jsonContent;
205    if (multiResourceBuild.value) {
206      jsonContent = multiResourceBuild.value;
207    } else {
208      jsonContent = readManifest(manifestFilePath);
209    }
210    const pages = jsonContent.pages;
211    if (pages === undefined) {
212      throw Error('ERROR: missing pages').message;
213    }
214    pages.forEach((element) => {
215      const sourcePath = element.replace(/^\.\/js\//, '');
216      const hmlPath = path.join(input, sourcePath + '.hml');
217      const aceSuperVisualPath = process.env.aceSuperVisualPath || '';
218      const visualPath = path.join(aceSuperVisualPath, sourcePath + '.visual');
219      const isHml = fs.existsSync(hmlPath);
220      const isVisual = fs.existsSync(visualPath);
221      const projectPath = process.env.projectPath;
222      if (isHml && isVisual) {
223        console.error('ERROR: ' + sourcePath + ' cannot both have hml && visual');
224      } else if (isHml) {
225        entryObj['./' + sourcePath] = path.resolve(projectPath, './' + sourcePath + '.hml?entry');
226      } else if (isVisual) {
227        entryObj['./' + sourcePath] = path.resolve(aceSuperVisualPath, './' + sourcePath +
228          '.visual?entry');
229      } else {
230        if (process.env.watchMode && process.env.watchMode === 'true') {
231          console.error('COMPILE RESULT:FAIL ');
232          console.error('ERROR: Invalid route ' + sourcePath +
233            '. Verify the route infomation in the main_pages.json' +
234            ' or build-profile.json5 file (in stage model) or thr config.json file (in FA model),' +
235            ' and then restart the Previewer.');
236          return;
237        } else {
238          throw Error('\u001b[31m' + 'ERROR: Invalid route ' + sourcePath +
239            '. Verify the route infomation in the main_pages.json' +
240            ' or build-profile.json5 file (in stage model) or thr config.json file (in FA model),' +
241            ' and then restart the Previewer.').message;
242        }
243      }
244    });
245  }
246  if (process.env.isPreview !== 'true' && process.env.DEVICE_LEVEL === 'rich') {
247    loadWorker(entryObj);
248  }
249  return entryObj;
250}
251
252let abilityEntryObj = {};
253function addAbilityEntryObj(moduleJson) {
254  abilityEntryObj = {};
255  const entranceFiles = readAbilityEntrance(moduleJson);
256  entranceFiles.forEach(filePath => {
257    const key = filePath.replace(/^\.\/js\//, './').replace(/\.js$/, '');
258    abilityEntryObj[key] = path.resolve(process.env.projectPath, '../', filePath);
259  });
260  return abilityEntryObj;
261}
262
263function readAbilityEntrance(moduleJson) {
264  const entranceFiles = [];
265  if (moduleJson.module) {
266    if (moduleJson.module.srcEntrance) {
267      entranceFiles.push(moduleJson.module.srcEntrance);
268    }
269    if (moduleJson.module.abilities && moduleJson.module.abilities.length) {
270      readEntrances(moduleJson.module.abilities, entranceFiles);
271    }
272    if (moduleJson.module.extensionAbilities && moduleJson.module.extensionAbilities.length) {
273      readEntrances(moduleJson.module.extensionAbilities, entranceFiles);
274    }
275  }
276  return entranceFiles;
277}
278
279function readEntrances(abilities, entranceFiles) {
280  abilities.forEach(ability => {
281    if ((!ability.type || ability.type !== 'form') && ability.srcEntrance) {
282      entranceFiles.push(ability.srcEntrance);
283    }
284  });
285}
286
287function readManifest(manifestFilePath) {
288  let manifest = {};
289  try {
290    if (fs.existsSync(manifestFilePath)) {
291      const jsonString = fs.readFileSync(manifestFilePath).toString();
292      manifest = JSON.parse(jsonString);
293    } else if (process.env.aceModuleJsonPath && fs.existsSync(process.env.aceModuleJsonPath)) {
294      buildManifest(manifest);
295   } else {
296    throw Error('\u001b[31m' + 'ERROR: the manifest.json or module.json is lost.' +
297      '\u001b[39m').message;
298   }
299  } catch (e) {
300    throw Error('\u001b[31m' + 'ERROR: the manifest.json or module.json file format is invalid.' +
301      '\u001b[39m').message;
302  }
303  return manifest;
304}
305
306function readModulePages(moduleJson) {
307  if ((moduleJson.module.uiSyntax === 'hml' || moduleJson.module.uiSyntax === 'js') && moduleJson.module.pages) {
308    const modulePagePath = path.resolve(process.env.aceProfilePath,
309      `${moduleJson.module.pages.replace(/\$profile\:/, '')}.json`);
310    if (fs.existsSync(modulePagePath)) {
311      const pagesConfig = JSON.parse(fs.readFileSync(modulePagePath, 'utf-8'));
312      return pagesConfig.src;
313    }
314  }
315}
316
317function readFormPages(moduleJson) {
318  const pages = [];
319  if (moduleJson.module.extensionAbilities && moduleJson.module.extensionAbilities.length) {
320    moduleJson.module.extensionAbilities.forEach(extensionAbility => {
321      if (extensionAbility.type && extensionAbility.type === 'form' && extensionAbility.metadata &&
322        extensionAbility.metadata.length) {
323        extensionAbility.metadata.forEach(item => {
324          if (item.resource && /\$profile\:/.test(item.resource)) {
325            parseFormConfig(item.resource, pages);
326          }
327        });
328      }
329    });
330  }
331  return pages;
332}
333
334function parseFormConfig(resource, pages) {
335  const resourceFile = path.resolve(process.env.aceProfilePath,
336    `${resource.replace(/\$profile\:/, '')}.json`);
337  if (fs.existsSync(resourceFile)) {
338    const pagesConfig = JSON.parse(fs.readFileSync(resourceFile, 'utf-8'));
339    if (pagesConfig.forms && pagesConfig.forms.length) {
340      pagesConfig.forms.forEach(form => {
341        if (form.src) {
342          pages.push(form.src);
343        }
344      });
345    }
346  }
347}
348
349function buildManifest(manifest) {
350  try {
351    const moduleJson =  JSON.parse(fs.readFileSync(process.env.aceModuleJsonPath).toString());
352    if (process.env.DEVICE_LEVEL === 'rich') {
353      manifest.pages = readModulePages(moduleJson);
354      manifest.abilityEntryObj = addAbilityEntryObj(moduleJson);
355    }
356    if (process.env.DEVICE_LEVEL === 'card') {
357      manifest.pages = readFormPages(moduleJson);
358    }
359    manifest.minPlatformVersion = moduleJson.app.minAPIVersion;
360  } catch (e) {
361    throw Error("\x1B[31m" + 'ERROR: the module.json file is lost or format is invalid.' +
362      "\x1B[39m").message;
363  }
364}
365
366function loadWorker(entryObj) {
367  if (workerFile) {
368    entryObj = Object.assign(entryObj, workerFile);
369  } else {
370    const workerPath = path.resolve(input, 'workers');
371    if (fs.existsSync(workerPath)) {
372      const workerFiles = [];
373      readFile(workerPath, workerFiles);
374      workerFiles.forEach((item) => {
375        if (/\.js$/.test(item)) {
376          const relativePath = path.relative(workerPath, item)
377            .replace(/\.js$/, '').replace(/\\/g, '/');
378          entryObj[`./workers/` + relativePath] = item;
379        }
380      });
381    }
382  }
383}
384
385function validateWorkOption() {
386  if (process.env.aceBuildJson && fs.existsSync(process.env.aceBuildJson)) {
387    const workerConfig = JSON.parse(fs.readFileSync(process.env.aceBuildJson).toString());
388    if(workerConfig.workers) {
389      return true;
390    }
391  }
392  return false;
393}
394
395function filterWorker(workerPath) {
396  return /\.(ts|js)$/.test(workerPath);
397}
398
399function readFile(dir, utFiles) {
400  try {
401    const files = fs.readdirSync(dir);
402    files.forEach((element) => {
403      const filePath = path.join(dir, element);
404      const status = fs.statSync(filePath);
405      if (status.isDirectory()) {
406        readFile(filePath, utFiles);
407      } else {
408        utFiles.push(filePath);
409      }
410    });
411  } catch (e) {
412    console.error(e.message);
413  }
414}
415
416function themeFileBuild(customThemePath, customThemeBuild) {
417  if (fs.existsSync(customThemePath)) {
418    const themeContent = JSON.parse(fs.readFileSync(customThemePath));
419    const newContent = {};
420    if (themeContent && themeContent['style']) {
421      newContent['style'] = {};
422      const styleContent = themeContent['style'];
423      Object.keys(styleContent).forEach(function(key) {
424        const customKey = CUSTOM_THEME_PROP_GROUPS[key];
425        const ohosKey = OHOS_THEME_PROP_GROUPS[key];
426        if (ohosKey) {
427          newContent['style'][ohosKey] = styleContent[key];
428        } else if (customKey) {
429          newContent['style'][customKey] = styleContent[key];
430        } else {
431          newContent['style'][key] = styleContent[key];
432        }
433      });
434      fs.writeFileSync(customThemeBuild, JSON.stringify(newContent, null, 2));
435      return true;
436    }
437  }
438  return false;
439}
440
441function readCSSInfo(watchCSSFiles) {
442  if (fs.existsSync(watchCSSFiles)) {
443    return JSON.parse(fs.readFileSync(watchCSSFiles));
444  } else {
445    return {};
446  }
447}
448
449function writeCSSInfo(filePath, infoObject) {
450  if (!fs.existsSync(path.resolve(filePath, '..'))) {
451    mkDir(path.resolve(filePath, '..'));
452  }
453  if (fs.existsSync(filePath)) {
454    fs.unlinkSync(filePath);
455  }
456  fs.writeFileSync(filePath, JSON.stringify(infoObject, null, 2));
457}
458
459function setCSSEntry(cssfiles, key) {
460  cssfiles["entry"] = cssfiles["entry"] || {};
461  if (fs.existsSync(path.join(input, key + '.css'))) {
462    cssfiles["entry"][path.join(input, key + '.css')] = true;
463  }
464}
465
466function checkTestRunner(projectPath, entryObj) {
467  const files = [];
468  walkSync(projectPath, files, entryObj, projectPath);
469}
470
471function walkSync(filePath_, files, entryObj, projectPath) {
472  fs.readdirSync(filePath_).forEach(function (name) {
473    const filePath = path.join(filePath_, name);
474    const stat = fs.statSync(filePath);
475    if (stat.isFile()) {
476      const extName = '.js';
477      if (path.extname(filePath) === extName) {
478        const key = filePath.replace(projectPath, '').replace(extName, '');
479        entryObj[`./${key}`] = filePath;
480      } else if (stat.isDirectory()) {
481        walkSync(filePath, files, entryObj, projectPath);
482      }
483    }
484  })
485}
486
487module.exports = ResourcePlugin;