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