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 cluster from 'cluster';
17import fs from 'fs';
18import path from 'path';
19import ts from 'typescript';
20import os from 'os';
21import sourceMap from 'source-map';
22
23import {
24  DEBUG,
25  ESMODULE,
26  EXTNAME_ETS,
27  EXTNAME_JS,
28  EXTNAME_TS,
29  EXTNAME_JSON,
30  EXTNAME_CJS,
31  EXTNAME_MJS,
32  TEMPORARY
33} from './common/ark_define';
34import {
35  nodeLargeOrEqualTargetVersion,
36  genTemporaryPath,
37  mkdirsSync,
38  validateFilePathLength,
39  toUnixPath,
40  isPackageModulesFile,
41  isFileInProject
42} from '../../utils';
43import {
44  tryMangleFileName,
45  writeObfuscatedSourceCode,
46  cleanUpUtilsObjects,
47  createAndStartEvent,
48  stopEvent
49} from '../../ark_utils';
50import { AOT_FULL, AOT_PARTIAL, AOT_TYPE } from '../../pre_define';
51import { SourceMapGenerator } from './generate_sourcemap';
52import {
53  handleObfuscatedFilePath,
54  enableObfuscateFileName,
55  enableObfuscatedFilePathConfig,
56  getRelativeSourcePath
57} from './common/ob_config_resolver';
58
59export let hasTsNoCheckOrTsIgnoreFiles: string[] = [];
60export let compilingEtsOrTsFiles: string[] = [];
61
62export function cleanUpFilesList(): void {
63  hasTsNoCheckOrTsIgnoreFiles = [];
64  compilingEtsOrTsFiles = [];
65}
66
67export function needAotCompiler(projectConfig: Object): boolean {
68  return projectConfig.compileMode === ESMODULE && (projectConfig.anBuildMode === AOT_FULL ||
69    projectConfig.anBuildMode === AOT_PARTIAL);
70}
71
72export function isAotMode(projectConfig: Object): boolean {
73  return projectConfig.compileMode === ESMODULE && (projectConfig.anBuildMode === AOT_FULL ||
74    projectConfig.anBuildMode === AOT_PARTIAL || projectConfig.anBuildMode === AOT_TYPE);
75}
76
77export function isDebug(projectConfig: Object): boolean {
78  return projectConfig.buildMode.toLowerCase() === DEBUG;
79}
80
81export function isBranchElimination(projectConfig: Object): boolean {
82  return projectConfig.branchElimination;
83}
84
85export function isMasterOrPrimary() {
86  return ((nodeLargeOrEqualTargetVersion(16) && cluster.isPrimary) ||
87    (!nodeLargeOrEqualTargetVersion(16) && cluster.isMaster));
88}
89
90export function changeFileExtension(file: string, targetExt: string, originExt = ''): string {
91  let currentExt = originExt.length === 0 ? path.extname(file) : originExt;
92  let fileWithoutExt = file.substring(0, file.lastIndexOf(currentExt));
93  return fileWithoutExt + targetExt;
94}
95
96function removeCacheFile(cacheFilePath: string, ext: string): void {
97  let filePath = toUnixPath(changeFileExtension(cacheFilePath, ext));
98  if (fs.existsSync(filePath)) {
99    fs.rmSync(filePath);
100  }
101}
102
103export function shouldETSOrTSFileTransformToJS(filePath: string, projectConfig: Object, metaInfo?: Object): boolean {
104  let cacheFilePath: string = genTemporaryPath(filePath, projectConfig.projectPath, projectConfig.cachePath,
105    projectConfig, metaInfo);
106
107  if (!projectConfig.processTs) {
108    removeCacheFile(cacheFilePath, EXTNAME_TS);
109    return true;
110  }
111
112  if (compilingEtsOrTsFiles.indexOf(filePath) !== -1) {
113    // file involves in compilation
114    const hasTsNoCheckOrTsIgnore = hasTsNoCheckOrTsIgnoreFiles.indexOf(filePath) !== -1;
115    // Remove cacheFile whose extension is different the target file
116    removeCacheFile(cacheFilePath, hasTsNoCheckOrTsIgnore ? EXTNAME_TS : EXTNAME_JS);
117    return hasTsNoCheckOrTsIgnore;
118  }
119
120  cacheFilePath = updateCacheFilePathIfEnableObuscatedFilePath(filePath, cacheFilePath, projectConfig);
121  cacheFilePath = toUnixPath(changeFileExtension(cacheFilePath, EXTNAME_JS));
122  return fs.existsSync(cacheFilePath);
123}
124
125// This API is used exclusively to determine whether a file needs to be converted into a JS file without removing the cached files,
126// which differs from API 'shouldETSOrTSFileTransformToJS'.
127export function shouldETSOrTSFileTransformToJSWithoutRemove(filePath: string, projectConfig: Object, metaInfo?: Object): boolean {
128  if (!projectConfig.processTs) {
129    return true;
130  }
131
132  if (compilingEtsOrTsFiles.indexOf(filePath) !== -1) {
133    // file involves in compilation
134    return hasTsNoCheckOrTsIgnoreFiles.indexOf(filePath) !== -1;
135  }
136
137  let cacheFilePath: string = genTemporaryPath(filePath, projectConfig.projectPath, projectConfig.cachePath,
138    projectConfig, metaInfo);
139  cacheFilePath = updateCacheFilePathIfEnableObuscatedFilePath(filePath, cacheFilePath, projectConfig);
140  cacheFilePath = toUnixPath(changeFileExtension(cacheFilePath, EXTNAME_JS));
141  return fs.existsSync(cacheFilePath);
142}
143
144function updateCacheFilePathIfEnableObuscatedFilePath(filePath: string, cacheFilePath: string, projectConfig: Object): string {
145  const isPackageModules = isPackageModulesFile(filePath, projectConfig);
146  if (enableObfuscatedFilePathConfig(isPackageModules, projectConfig) && enableObfuscateFileName(isPackageModules, projectConfig)) {
147    return handleObfuscatedFilePath(cacheFilePath, isPackageModules, projectConfig);
148  }
149  return cacheFilePath;
150}
151
152export async function writeFileContentToTempDir(id: string, content: string, projectConfig: Object,
153  logger: Object, parentEvent: Object, metaInfo: Object): Promise<void> {
154  if (isCommonJsPluginVirtualFile(id)) {
155    return;
156  }
157
158  if (!isCurrentProjectFiles(id, projectConfig)) {
159    return;
160  }
161
162  let filePath: string;
163  if (projectConfig.compileHar) {
164    // compileShared: compile shared har of project
165    filePath = genTemporaryPath(id,
166      projectConfig.compileShared ? projectConfig.projectRootPath : projectConfig.moduleRootPath,
167      projectConfig.compileShared ? path.resolve(projectConfig.aceModuleBuild, '../etsFortgz') : projectConfig.cachePath,
168      projectConfig, metaInfo, projectConfig.compileShared);
169  } else {
170    filePath = genTemporaryPath(id, projectConfig.projectPath, projectConfig.cachePath, projectConfig, metaInfo);
171  }
172
173  const eventWriteFileContent = createAndStartEvent(parentEvent, 'write file content');
174  switch (path.extname(id)) {
175    case EXTNAME_ETS:
176    case EXTNAME_TS:
177    case EXTNAME_JS:
178    case EXTNAME_MJS:
179    case EXTNAME_CJS:
180      await writeFileContent(id, filePath, content, projectConfig, logger, metaInfo);
181      break;
182    case EXTNAME_JSON:
183      const newFilePath: string = tryMangleFileName(filePath, projectConfig, id);
184      mkdirsSync(path.dirname(newFilePath));
185      fs.writeFileSync(newFilePath, content ?? '');
186      break;
187    default:
188      break;
189  }
190  stopEvent(eventWriteFileContent);
191}
192
193async function writeFileContent(sourceFilePath: string, filePath: string, content: string,
194  projectConfig: Object, logger: Object, metaInfo?: Object): Promise<void> {
195  if (!isSpecifiedExt(sourceFilePath, EXTNAME_JS)) {
196    filePath = changeFileExtension(filePath, EXTNAME_JS);
197  }
198
199  if (!isDebug(projectConfig)) {
200    const relativeSourceFilePath: string = getRelativeSourcePath(sourceFilePath, projectConfig.projectRootPath,
201      metaInfo?.belongProjectPath);
202    await writeObfuscatedSourceCode({content: content, buildFilePath: filePath,
203      relativeSourceFilePath: relativeSourceFilePath, originSourceFilePath: sourceFilePath, rollupModuleId: sourceFilePath},
204      logger, projectConfig, SourceMapGenerator.getInstance().getSourceMaps());
205    return;
206  }
207  mkdirsSync(path.dirname(filePath));
208  fs.writeFileSync(filePath, content, 'utf-8');
209}
210
211export function getEs2abcFileThreadNumber(): number {
212  const fileThreads: number = os.cpus().length < 16 ? os.cpus().length : 16;
213  return fileThreads;
214}
215
216export function isCommonJsPluginVirtualFile(filePath: string): boolean {
217  // rollup uses commonjs plugin to handle commonjs files,
218  // which will automatic generate files like 'jsfile.js?commonjs-exports'
219  return filePath.includes('\x00');
220}
221
222export function isCurrentProjectFiles(filePath: string, projectConfig: Object): boolean {
223  if (projectConfig.rootPathSet) {
224    for (const projectRootPath of projectConfig.rootPathSet) {
225      if (isFileInProject(filePath, projectRootPath)) {
226        return true;
227      }
228    }
229    return false;
230  } 
231  return isFileInProject(filePath, projectConfig.projectRootPath);
232}
233
234export function genTemporaryModuleCacheDirectoryForBundle(projectConfig: Object): string {
235  const buildDirArr: string[] = projectConfig.aceModuleBuild.split(path.sep);
236  const abilityDir: string = buildDirArr[buildDirArr.length - 1];
237  const temporaryModuleCacheDirPath: string = path.join(projectConfig.cachePath, TEMPORARY, abilityDir);
238  mkdirsSync(temporaryModuleCacheDirPath);
239
240  return temporaryModuleCacheDirPath;
241}
242
243export function isSpecifiedExt(filePath: string, fileExtendName: string) {
244  return path.extname(filePath) === fileExtendName;
245}
246
247export function genCachePath(tailName: string, projectConfig: Object, logger: Object): string {
248  const pathName: string = projectConfig.cachePath !== undefined ?
249    path.join(projectConfig.cachePath, TEMPORARY, tailName) : path.join(projectConfig.aceModuleBuild, tailName);
250  mkdirsSync(path.dirname(pathName));
251
252  validateFilePathLength(pathName, logger);
253  return pathName;
254}
255
256export function isTsOrEtsSourceFile(file: string): boolean {
257  return /(?<!\.d)\.[e]?ts$/.test(file);
258}
259
260export function isJsSourceFile(file: string): boolean {
261  return /\.[cm]?js$/.test(file);
262}
263
264export function isJsonSourceFile(file: string): boolean {
265  return /\.json$/.test(file);
266}
267
268export async function updateSourceMap(originMap: sourceMap.RawSourceMap, newMap: sourceMap.RawSourceMap): Promise<any> {
269  if (!originMap) {
270    return newMap;
271  }
272  if (!newMap) {
273    return originMap;
274  }
275  const originConsumer: sourceMap.SourceMapConsumer = await new sourceMap.SourceMapConsumer(originMap);
276  const newConsumer: sourceMap.SourceMapConsumer = await new sourceMap.SourceMapConsumer(newMap);
277  const newMappingList: sourceMap.MappingItem[] = [];
278  newConsumer.eachMapping((mapping: sourceMap.MappingItem) => {
279    if (mapping.originalLine == null) {
280      return;
281    }
282    const originalPos =
283      originConsumer.originalPositionFor({ line: mapping.originalLine, column: mapping.originalColumn });
284    if (originalPos.source == null) {
285      return;
286    }
287    mapping.originalLine = originalPos.line;
288    mapping.originalColumn = originalPos.column;
289    newMappingList.push(mapping);
290  });
291  const updatedGenerator: sourceMap.SourceMapGenerator = sourceMap.SourceMapGenerator.fromSourceMap(newConsumer);
292  updatedGenerator._file = originMap.file;
293  updatedGenerator._mappings._array = newMappingList;
294  return JSON.parse(updatedGenerator.toString());
295}
296
297export function hasArkDecorator(node: ts.MethodDeclaration | ts.FunctionDeclaration |
298  ts.StructDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration, decortorName: string): boolean {
299  const decorators: readonly ts.Decorator[] = ts.getAllDecorators(node);
300  if (decorators && decorators.length) {
301    for (let i = 0; i < decorators.length; i++) {
302      const originalDecortor: string = decorators[i].getText().replace(/\(.*\)$/, '').replace(/\s*/g, '').trim();
303      return originalDecortor === decortorName;
304    }
305  }
306  return false;
307}
308
309export const utUtils = {
310  writeFileContent
311};