• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023 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
16import path from 'path';
17import fs from 'fs';
18import { minify, MinifyOutput } from 'terser';
19
20import { OH_MODULES } from './fast_build/ark_compiler/common/ark_define';
21import {
22  PACKAGES,
23  TEMPORARY,
24  ZERO,
25  ONE,
26  EXTNAME_JS,
27  EXTNAME_TS,
28  EXTNAME_MJS,
29  EXTNAME_CJS,
30  EXTNAME_ABC,
31  EXTNAME_ETS,
32  EXTNAME_TS_MAP,
33  EXTNAME_JS_MAP,
34  ESMODULE,
35  FAIL,
36  TS2ABC,
37  ES2ABC,
38  EXTNAME_PROTO_BIN,
39  NATIVE_MODULE,
40} from './pre_define';
41import {
42  isMac,
43  isWindows,
44  isPackageModulesFile,
45  genTemporaryPath,
46  getExtensionIfUnfullySpecifiedFilepath,
47  mkdirsSync,
48  toUnixPath,
49  validateFilePathLength
50} from './utils';
51import {
52  projectConfig
53} from '../main';
54
55const red: string = '\u001b[31m';
56const reset: string = '\u001b[39m';
57
58export const SRC_MAIN: string = 'src/main';
59
60export var newSourceMaps: Object = {};
61export const packageCollection: Map<string, Array<string>> = new Map();
62
63export function getOhmUrlByFilepath(filePath: string, projectConfig: any, logger: any, namespace?: string): string {
64  // remove '\x00' from the rollup virtual commonjs file's filePath
65  if (filePath.startsWith('\x00')) {
66    filePath = filePath.replace('\x00', '');
67  }
68  let unixFilePath: string = toUnixPath(filePath);
69  unixFilePath = unixFilePath.substring(0, filePath.lastIndexOf('.')); // remove extension
70  const REG_PROJECT_SRC: RegExp = /(\S+)\/src\/(?:main|ohosTest)\/(ets|js)\/(\S+)/;
71
72  const packageInfo: string[] = getPackageInfo(projectConfig.aceModuleJsonPath);
73  const bundleName: string = packageInfo[0];
74  const moduleName: string = packageInfo[1];
75  const moduleRootPath: string = toUnixPath(projectConfig.modulePathMap[moduleName]);
76  const projectRootPath: string = toUnixPath(projectConfig.projectRootPath);
77  // case1: /entry/src/main/ets/xxx/yyy     ---> @bundle:<bundleName>/entry/ets/xxx/yyy
78  // case2: /entry/src/ohosTest/ets/xxx/yyy ---> @bundle:<bundleName>/entry_test@entry/ets/xxx/yyy
79  // case3: /node_modules/xxx/yyy           ---> @package:pkg_modules/xxx/yyy
80  // case4: /entry/node_modules/xxx/yyy     ---> @package:pkg_modules@entry/xxx/yyy
81  // case5: /library/node_modules/xxx/yyy   ---> @package:pkg_modules@library/xxx/yyy
82  // case6: /library/index.ts               ---> @bundle:<bundleName>/library/index
83  const projectFilePath: string = unixFilePath.replace(projectRootPath, '');
84  const packageDir: string = projectConfig.packageDir;
85  const result: RegExpMatchArray | null = projectFilePath.match(REG_PROJECT_SRC);
86  if (result && result[1].indexOf(packageDir) === -1) {
87    let langType: string = result[2];
88    let relativePath: string = result[3];
89    // case7: /entry/src/main/ets/xxx/src/main/js/yyy ---> @bundle:<bundleName>/entry/ets/xxx/src/main/js/yyy
90    const REG_SRC_MAIN: RegExp = /src\/(?:main|ohosTest)\/(ets|js)\//;
91    const srcMainIndex: number = result[1].search(REG_SRC_MAIN);
92    if (srcMainIndex !== -1) {
93      relativePath = projectFilePath.substring(srcMainIndex).replace(REG_SRC_MAIN, '');
94      langType = projectFilePath.replace(relativePath, '').match(REG_SRC_MAIN)[1];
95    }
96    if (namespace && moduleName !== namespace) {
97      return `${bundleName}/${moduleName}@${namespace}/${langType}/${relativePath}`;
98    }
99    return `${bundleName}/${moduleName}/${langType}/${relativePath}`;
100  }
101
102  if (projectFilePath.indexOf(packageDir) !== -1) {
103    if (process.env.compileTool === 'rollup') {
104      const tryProjectPkg: string = toUnixPath(path.join(projectRootPath, packageDir));
105      if (unixFilePath.indexOf(tryProjectPkg) !== -1) {
106        return unixFilePath.replace(tryProjectPkg, `${packageDir}`).replace(new RegExp(packageDir, 'g'), PACKAGES);
107      }
108      // iterate the modulePathMap to find the moudleName which contains the pkg_module's file
109      for (const moduleName in projectConfig.modulePathMap) {
110        const modulePath: string = projectConfig.modulePathMap[moduleName];
111        const tryModulePkg: string = toUnixPath(path.resolve(modulePath, packageDir));
112        if (unixFilePath.indexOf(tryModulePkg) !== -1) {
113          return unixFilePath.replace(tryModulePkg, `${packageDir}@${moduleName}`).replace(
114            new RegExp(packageDir, 'g'), PACKAGES);
115        }
116      }
117
118      logger.error(red, `ArkTS:ERROR Failed to get an resolved OhmUrl by filepath "${filePath}"`, reset);
119      return filePath;
120    }
121
122    // webpack with old implematation
123    const tryProjectPkg: string = toUnixPath(path.join(projectRootPath, packageDir));
124    if (unixFilePath.indexOf(tryProjectPkg) !== -1) {
125      return unixFilePath.replace(tryProjectPkg, `${packageDir}/${ONE}`).replace(new RegExp(packageDir, 'g'), PACKAGES);
126    }
127
128    const tryModulePkg: string = toUnixPath(path.join(moduleRootPath, packageDir));
129    if (unixFilePath.indexOf(tryModulePkg) !== -1) {
130      return unixFilePath.replace(tryModulePkg, `${packageDir}/${ZERO}`).replace(new RegExp(packageDir, 'g'), PACKAGES);
131    }
132  }
133
134  for (const key in projectConfig.modulePathMap) {
135    const moduleRootPath: string = toUnixPath(projectConfig.modulePathMap[key]);
136    if (unixFilePath.indexOf(moduleRootPath + '/') !== -1) {
137      const relativeModulePath: string = unixFilePath.replace(moduleRootPath + '/', '');
138      if (namespace && moduleName !== namespace) {
139        return `${bundleName}/${moduleName}@${namespace}/${relativeModulePath}`;
140      }
141      return `${bundleName}/${moduleName}/${relativeModulePath}`;
142    }
143  }
144
145  logger.error(red, `ArkTS:ERROR Failed to get an resolved OhmUrl by filepath "${filePath}"`, reset);
146  return filePath;
147}
148
149export function getOhmUrlBySystemApiOrLibRequest(moduleRequest: string) : string
150{
151  const REG_SYSTEM_MODULE: RegExp = /@(system|ohos)\.(\S+)/;
152  const REG_LIB_SO: RegExp = /lib(\S+)\.so/;
153
154  if (REG_SYSTEM_MODULE.test(moduleRequest.trim())) {
155    return moduleRequest.replace(REG_SYSTEM_MODULE, (_, moduleType, systemKey) => {
156      const systemModule: string = `${moduleType}.${systemKey}`;
157      if (NATIVE_MODULE.has(systemModule)) {
158        return `@native:${systemModule}`;
159      } else {
160        return `@ohos:${systemKey}`;
161      };
162    });
163  }
164  if (REG_LIB_SO.test(moduleRequest.trim())) {
165    return moduleRequest.replace(REG_LIB_SO, (_, libsoKey) => {
166      return `@app:${projectConfig.bundleName}/${projectConfig.moduleName}/${libsoKey}`;
167    });
168  }
169
170  return undefined;
171}
172
173export function genSourceMapFileName(temporaryFile: string): string {
174  let abcFile: string = temporaryFile;
175  if (temporaryFile.endsWith(EXTNAME_TS)) {
176    abcFile = temporaryFile.replace(/\.ts$/, EXTNAME_TS_MAP);
177  } else {
178    abcFile = temporaryFile.replace(/\.js$/, EXTNAME_JS_MAP);
179  }
180  return abcFile;
181}
182
183export function getBuildModeInLowerCase(projectConfig: any): string {
184  return (process.env.compileTool === 'rollup' ?  projectConfig.buildMode : projectConfig.buildArkMode).toLowerCase();
185}
186
187export function writeFileSyncByString(sourcePath: string, sourceCode: string, projectConfig: any, logger: any): void {
188  const filePath: string = genTemporaryPath(sourcePath, projectConfig.projectPath, process.env.cachePath, projectConfig);
189  if (filePath.length === 0) {
190    return;
191  }
192  mkdirsSync(path.dirname(filePath));
193  if (/\.js$/.test(sourcePath)) {
194    sourceCode = transformModuleSpecifier(sourcePath, sourceCode, projectConfig);
195    if (projectConfig.buildArkMode === 'debug') {
196      fs.writeFileSync(filePath, sourceCode);
197      return;
198    }
199    writeMinimizedSourceCode(sourceCode, filePath, logger);
200  }
201  if (/\.json$/.test(sourcePath)) {
202    fs.writeFileSync(filePath, sourceCode);
203  }
204}
205
206export function transformModuleSpecifier(sourcePath: string, sourceCode: string, projectConfig: any): string {
207  // replace relative moduleSpecifier with ohmURl
208  const REG_RELATIVE_DEPENDENCY: RegExp = /(?:import|from)(?:\s*)['"]((?:\.\/|\.\.\/)[^'"]+|(?:\.\/?|\.\.\/?))['"]/g;
209  const REG_HAR_DEPENDENCY: RegExp = /(?:import|from)(?:\s*)['"]([^\.\/][^'"]+)['"]/g;
210  // replace requireNapi and requireNativeModule with import
211  const REG_REQUIRE_NATIVE_MODULE: RegExp = /var (\S+) = globalThis.requireNativeModule\(['"](\S+)['"]\);/g;
212  const REG_REQUIRE_NAPI_APP: RegExp = /var (\S+) = globalThis.requireNapi\(['"](\S+)['"], true, ['"](\S+)['"]\);/g;
213  const REG_REQUIRE_NAPI_OHOS: RegExp = /var (\S+) = globalThis.requireNapi\(['"](\S+)['"]\);/g;
214
215  return sourceCode.replace(REG_HAR_DEPENDENCY, (item, moduleRequest) => {
216    return replaceHarDependency(item, moduleRequest, projectConfig);
217  }).replace(REG_RELATIVE_DEPENDENCY, (item, moduleRequest) => {
218    return replaceRelativeDependency(item, moduleRequest, toUnixPath(sourcePath), projectConfig);
219  }).replace(REG_REQUIRE_NATIVE_MODULE, (_, moduleRequest, moduleName) => {
220    return `import ${moduleRequest} from '@native:${moduleName}';`;
221  }).replace(REG_REQUIRE_NAPI_APP, (_, moduleRequest, soName, moudlePath) => {
222    return `import ${moduleRequest} from '@app:${moudlePath}/${soName}';`;
223  }).replace(REG_REQUIRE_NAPI_OHOS, (_, moduleRequest, moduleName) => {
224    return `import ${moduleRequest} from '@ohos:${moduleName}';`;
225  });
226}
227
228export function getOhmUrlByHarName(moduleRequest: string, projectConfig: any): string | undefined {
229  if (projectConfig.harNameOhmMap) {
230    // case1: "@ohos/lib" ---> "@bundle:bundleName/lib/ets/index"
231    if (projectConfig.harNameOhmMap.hasOwnProperty(moduleRequest)) {
232      return projectConfig.harNameOhmMap[moduleRequest];
233    }
234    // case2: "@ohos/lib/src/main/ets/pages/page1" ---> "@bundle:bundleName/lib/ets/pages/page1"
235    for (const harName in projectConfig.harNameOhmMap) {
236      if (moduleRequest.startsWith(harName + '/')) {
237        const idx: number = projectConfig.harNameOhmMap[harName].split('/', 2).join('/').length;
238        const harOhmName: string = projectConfig.harNameOhmMap[harName].substring(0, idx);
239        if (moduleRequest.indexOf(harName + '/' + SRC_MAIN) === 0) {
240          return moduleRequest.replace(harName + '/' + SRC_MAIN, harOhmName);
241        } else {
242          return moduleRequest.replace(harName, harOhmName);
243        }
244      }
245    }
246  }
247  return undefined;
248}
249
250function replaceHarDependency(item:string, moduleRequest: string, projectConfig: any): string {
251  const harOhmUrl: string | undefined = getOhmUrlByHarName(moduleRequest, projectConfig);
252  if (harOhmUrl !== undefined) {
253    return item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => {
254      return quotation + harOhmUrl + quotation;
255    });
256  }
257  return item;
258}
259
260function locateActualFilePathWithModuleRequest(absolutePath: string): string {
261  if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isDirectory()) {
262    return absolutePath
263  }
264
265  const filePath: string = absolutePath + getExtensionIfUnfullySpecifiedFilepath(absolutePath);
266  if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
267    return absolutePath;
268  }
269
270  return path.join(absolutePath, 'index');
271}
272
273function replaceRelativeDependency(item:string, moduleRequest: string, sourcePath: string, projectConfig: any): string {
274  if (sourcePath && projectConfig.compileMode === ESMODULE) {
275    // remove file extension from moduleRequest
276    const SUFFIX_REG: RegExp = /\.(?:[cm]?js|[e]?ts|json)$/;
277    moduleRequest = moduleRequest.replace(SUFFIX_REG, '');
278
279    // normalize the moduleRequest
280    item = item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => {
281      let normalizedModuleRequest: string = toUnixPath(path.normalize(moduleRequest));
282      if (moduleRequest.startsWith("./")) {
283        normalizedModuleRequest = "./" + normalizedModuleRequest;
284      }
285      return quotation + normalizedModuleRequest + quotation;
286    });
287
288    const filePath: string =
289      locateActualFilePathWithModuleRequest(path.resolve(path.dirname(sourcePath), moduleRequest));
290    const result: RegExpMatchArray | null =
291      filePath.match(/(\S+)(\/|\\)src(\/|\\)(?:main|ohosTest)(\/|\\)(ets|js)(\/|\\)(\S+)/);
292    if (result && projectConfig.aceModuleJsonPath) {
293      const npmModuleIdx: number = result[1].search(/(\/|\\)node_modules(\/|\\)/);
294      const projectRootPath: string = projectConfig.projectRootPath;
295      if (npmModuleIdx === -1 || npmModuleIdx === projectRootPath.search(/(\/|\\)node_modules(\/|\\)/)) {
296        const packageInfo: string[] = getPackageInfo(projectConfig.aceModuleJsonPath);
297        const bundleName: string = packageInfo[0];
298        const moduleName: string = packageInfo[1];
299        moduleRequest = `@bundle:${bundleName}/${moduleName}/${result[5]}/${toUnixPath(result[7])}`;
300        item = item.replace(/(['"])(?:\S+)['"]/, (_, quotation) => {
301          return quotation + moduleRequest + quotation;
302        });
303      }
304    }
305  }
306  return item;
307}
308
309export async function writeMinimizedSourceCode(content: string, filePath: string, logger: any,
310  isHar: boolean = false, relativeSourceFilePath: string = '', rollupNewSourceMaps: any = {}): Promise<void> {
311  let result: MinifyOutput;
312  try {
313    const minifyOptions = {
314      compress: {
315        join_vars: false,
316        sequences: 0,
317        directives: false
318      }
319    };
320    if (!isHar) {
321      minifyOptions['format'] = {
322        semicolons: false,
323        beautify: true,
324        indent_level: 2
325      };
326      if (process.env.compileTool === 'rollup' && relativeSourceFilePath.length > 0) {
327        minifyOptions['sourceMap'] = {
328          content: rollupNewSourceMaps[relativeSourceFilePath],
329          asObject: true
330        };
331      }
332    }
333    result = await minify(content, minifyOptions);
334  } catch {
335    logger.error(red, `ArkTS:ERROR Failed to source code obfuscation.`, reset);
336    process.exit(FAIL);
337  }
338  if (process.env.compileTool === 'rollup' && result.map) {
339    result.map.sourcesContent && delete result.map.sourcesContent;
340    result.map.sources = [relativeSourceFilePath];
341    rollupNewSourceMaps[relativeSourceFilePath] = result.map;
342  }
343  fs.writeFileSync(filePath, result.code);
344}
345
346export function genBuildPath(filePath: string, projectPath: string, buildPath: string, projectConfig: any): string {
347  filePath = toUnixPath(filePath);
348  if (filePath.endsWith(EXTNAME_MJS)) {
349    filePath = filePath.replace(/\.mjs$/, EXTNAME_JS);
350  }
351  if (filePath.endsWith(EXTNAME_CJS)) {
352    filePath = filePath.replace(/\.cjs$/, EXTNAME_JS);
353  }
354  projectPath = toUnixPath(projectPath);
355
356  if (isPackageModulesFile(filePath, projectConfig)) {
357    const packageDir: string = projectConfig.packageDir;
358    const fakePkgModulesPath: string = toUnixPath(path.join(projectConfig.projectRootPath, packageDir));
359    let output: string = '';
360    if (filePath.indexOf(fakePkgModulesPath) === -1) {
361      const hapPath: string = toUnixPath(projectConfig.projectRootPath);
362      const tempFilePath: string = filePath.replace(hapPath, '');
363      const sufStr: string = tempFilePath.substring(tempFilePath.indexOf(packageDir) + packageDir.length + 1);
364      output = path.join(projectConfig.nodeModulesPath, ZERO, sufStr);
365    } else {
366      output = filePath.replace(fakePkgModulesPath, path.join(projectConfig.nodeModulesPath, ONE));
367    }
368    return output;
369  }
370
371  if (filePath.indexOf(projectPath) !== -1) {
372    const sufStr: string = filePath.replace(projectPath, '');
373    const output: string = path.join(buildPath, sufStr);
374    return output;
375  }
376
377  return '';
378}
379
380export function getPackageInfo(configFile: string): Array<string> {
381  if (packageCollection.has(configFile)) {
382    return packageCollection.get(configFile);
383  }
384  const data: any = JSON.parse(fs.readFileSync(configFile).toString());
385  const bundleName: string = data.app.bundleName;
386  const moduleName: string = data.module.name;
387  packageCollection.set(configFile, [bundleName, moduleName]);
388  return [bundleName, moduleName];
389}
390
391export function generateSourceFilesToTemporary(sourcePath: string, sourceContent: string, sourceMap: any,
392  projectConfig: any, logger: any): void {
393  let jsFilePath: string = genTemporaryPath(sourcePath, projectConfig.projectPath, process.env.cachePath, projectConfig);
394  if (jsFilePath.length === 0) {
395    return;
396  }
397  if (jsFilePath.endsWith(EXTNAME_ETS)) {
398    jsFilePath = jsFilePath.replace(/\.ets$/, EXTNAME_JS);
399  } else {
400    jsFilePath = jsFilePath.replace(/\.ts$/, EXTNAME_JS);
401  }
402  let sourceMapFile: string = genSourceMapFileName(jsFilePath);
403  if (sourceMapFile.length > 0 && projectConfig.buildArkMode === 'debug') {
404    let source = toUnixPath(sourcePath).replace(toUnixPath(projectConfig.projectRootPath) + '/', '');
405    // adjust sourceMap info
406    sourceMap.sources = [source];
407    sourceMap.file = path.basename(sourceMap.file);
408    delete sourceMap.sourcesContent;
409    newSourceMaps[source] = sourceMap;
410  }
411  sourceContent = transformModuleSpecifier(sourcePath, sourceContent, projectConfig);
412
413  mkdirsSync(path.dirname(jsFilePath));
414  if (projectConfig.buildArkMode === 'debug') {
415    fs.writeFileSync(jsFilePath, sourceContent);
416    return;
417  }
418
419  writeMinimizedSourceCode(sourceContent, jsFilePath, logger);
420}
421
422export function genAbcFileName(temporaryFile: string): string {
423  let abcFile: string = temporaryFile;
424  if (temporaryFile.endsWith(EXTNAME_TS)) {
425    abcFile = temporaryFile.replace(/\.ts$/, EXTNAME_ABC);
426  } else {
427    abcFile = temporaryFile.replace(/\.js$/, EXTNAME_ABC);
428  }
429  return abcFile;
430}
431
432export function isOhModules(projectConfig: any): boolean {
433  return projectConfig.packageDir === OH_MODULES;
434}
435
436export function isEs2Abc(projectConfig: any): boolean {
437  return projectConfig.pandaMode === ES2ABC || projectConfig.pandaMode === "undefined" ||
438    projectConfig.pandaMode === undefined;
439}
440
441export function isTs2Abc(projectConfig: any): boolean {
442  return projectConfig.pandaMode === TS2ABC;
443}
444
445export function genProtoFileName(temporaryFile: string): string {
446  return temporaryFile.replace(/\.(?:[tj]s|json)$/, EXTNAME_PROTO_BIN);
447}
448
449export function genMergeProtoFileName(temporaryFile: string): string {
450  let protoTempPathArr: string[] = temporaryFile.split(TEMPORARY);
451  const sufStr: string = protoTempPathArr[protoTempPathArr.length - 1];
452  let protoBuildPath: string = path.join(process.env.cachePath, "protos", sufStr);
453
454  return protoBuildPath;
455}
456
457export function removeDuplicateInfo(moduleInfos: Array<any>): Array<any> {
458  const tempModuleInfos: any[] = Array<any>();
459  moduleInfos.forEach((item) => {
460    let check: boolean = tempModuleInfos.every((newItem) => {
461      return item.tempFilePath !== newItem.tempFilePath;
462    });
463    if (check) {
464      tempModuleInfos.push(item);
465    }
466  });
467  moduleInfos = tempModuleInfos;
468
469  return moduleInfos;
470}
471
472export function buildCachePath(tailName: string, projectConfig: any, logger: any): string {
473  let pathName: string = process.env.cachePath !== undefined ?
474      path.join(projectConfig.cachePath, tailName) : path.join(projectConfig.aceModuleBuild, tailName);
475  validateFilePathLength(pathName, logger);
476  return pathName;
477}
478
479export function getArkBuildDir(arkDir: string): string {
480  if (isWindows()) {
481    return path.join(arkDir, 'build-win');
482  } else if (isMac()) {
483    return path.join(arkDir, 'build-mac');
484  } else {
485    return path.join(arkDir, 'build');
486  }
487}
488
489export function getBuildBinDir(arkDir: string): string {
490  return path.join(getArkBuildDir(arkDir), 'bin');
491}
492