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 16import path from 'path'; 17import ts from 'typescript'; 18import fs from 'fs'; 19import os from 'os'; 20import uglifyJS from 'uglify-js'; 21 22import { 23 projectConfig, 24 partialUpdateConfig 25} from '../main'; 26import { createHash } from 'crypto'; 27import { 28 AUXILIARY, 29 EXTNAME_ETS, 30 EXTNAME_CJS, 31 EXTNAME_MJS, 32 EXTNAME_JS, 33 MAIN, 34 FAIL, 35 TEMPORARY, 36 ESMODULE, 37 $$ 38} from './pre_define'; 39 40export enum LogType { 41 ERROR = 'ERROR', 42 WARN = 'WARN', 43 NOTE = 'NOTE' 44} 45export const TEMPORARYS: string = 'temporarys'; 46export const BUILD: string = 'build'; 47export const SRC_MAIN: string = 'src/main'; 48 49const red: string = '\u001b[31m'; 50const reset: string = '\u001b[39m'; 51 52const WINDOWS: string = 'Windows_NT'; 53const LINUX: string = 'Linux'; 54const MAC: string = 'Darwin'; 55 56export interface LogInfo { 57 type: LogType, 58 message: string, 59 pos?: number, 60 line?: number, 61 column?: number, 62 fileName?: string 63} 64 65export const repeatLog: Map<string, LogInfo> = new Map(); 66 67export class FileLog { 68 private _sourceFile: ts.SourceFile; 69 private _errors: LogInfo[] = []; 70 71 public get sourceFile() { 72 return this._sourceFile; 73 } 74 75 public set sourceFile(newValue: ts.SourceFile) { 76 this._sourceFile = newValue; 77 } 78 79 public get errors() { 80 return this._errors; 81 } 82 83 public set errors(newValue: LogInfo[]) { 84 this._errors = newValue; 85 } 86} 87 88export function emitLogInfo(loader: any, infos: LogInfo[], fastBuild: boolean = false, 89 resourcePath: string = null): void { 90 if (infos && infos.length) { 91 infos.forEach((item) => { 92 switch (item.type) { 93 case LogType.ERROR: 94 fastBuild ? loader.error('\u001b[31m' + getMessage(item.fileName || resourcePath, item, true)) : 95 loader.emitError(getMessage(item.fileName || loader.resourcePath, item)); 96 break; 97 case LogType.WARN: 98 fastBuild ? loader.warn('\u001b[33m' + getMessage(item.fileName || resourcePath, item, true)) : 99 loader.emitWarning(getMessage(item.fileName || loader.resourcePath, item)); 100 break; 101 case LogType.NOTE: 102 fastBuild ? loader.info('\u001b[34m' + getMessage(item.fileName || resourcePath, item, true)) : 103 loader.emitWarning(getMessage(loader.resourcePath, item)); 104 break; 105 } 106 }); 107 } 108} 109 110export function addLog(type: LogType, message: string, pos: number, log: LogInfo[], 111 sourceFile: ts.SourceFile) { 112 const posOfNode: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(pos); 113 log.push({ 114 type: type, 115 message: message, 116 line: posOfNode.line + 1, 117 column: posOfNode.character + 1, 118 fileName: sourceFile.fileName 119 }); 120} 121 122export function getMessage(fileName: string, info: LogInfo, fastBuild: boolean = false): string { 123 let message: string; 124 if (info.line && info.column) { 125 message = `BUILD${info.type} File: ${fileName}:${info.line}:${info.column}\n ${info.message}`; 126 } else { 127 message = `BUILD${info.type} File: ${fileName}\n ${info.message}`; 128 } 129 if (fastBuild) { 130 message = message.replace(/^BUILD/, 'ArkTS:'); 131 } 132 return message; 133} 134 135export function getTransformLog(transformLog: FileLog): LogInfo[] { 136 const sourceFile: ts.SourceFile = transformLog.sourceFile; 137 const logInfos: LogInfo[] = transformLog.errors.map((item) => { 138 if (item.pos) { 139 if (!item.column || !item.line) { 140 const posOfNode: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(item.pos); 141 item.line = posOfNode.line + 1; 142 item.column = posOfNode.character + 1; 143 } 144 } else { 145 item.line = item.line || undefined; 146 item.column = item.column || undefined; 147 } 148 if (!item.fileName) { 149 item.fileName = sourceFile.fileName; 150 } 151 return item; 152 }); 153 return logInfos; 154} 155 156class ComponentInfo { 157 private _id: number = 0; 158 private _componentNames: Set<string> = new Set(['ForEach']); 159 public set id(id: number) { 160 this._id = id; 161 } 162 public get id() { 163 return this._id; 164 } 165 public set componentNames(componentNames: Set<string>) { 166 this._componentNames = componentNames; 167 } 168 public get componentNames() { 169 return this._componentNames; 170 } 171} 172 173export const componentInfo: ComponentInfo = new ComponentInfo(); 174 175export function hasDecorator(node: ts.MethodDeclaration | ts.FunctionDeclaration | 176 ts.StructDeclaration | ts.ClassDeclaration, decortorName: string, customBuilder?: ts.Decorator[]): boolean { 177 if (node.decorators && node.decorators.length) { 178 for (let i = 0; i < node.decorators.length; i++) { 179 if (node.decorators[i].getText().replace(/\(.*\)$/, '').trim() === decortorName) { 180 if (customBuilder) { 181 customBuilder.push(...node.decorators.slice(i + 1), ...node.decorators.slice(0, i)); 182 } 183 return true; 184 } 185 } 186 } 187 return false; 188} 189 190const STATEMENT_EXPECT: number = 1128; 191const SEMICOLON_EXPECT: number = 1005; 192const STATESTYLES_EXPECT: number = 1003; 193export const IGNORE_ERROR_CODE: number[] = [STATEMENT_EXPECT, SEMICOLON_EXPECT, STATESTYLES_EXPECT]; 194 195export function readFile(dir: string, utFiles: string[]) { 196 try { 197 const files: string[] = fs.readdirSync(dir); 198 files.forEach((element) => { 199 const filePath: string = path.join(dir, element); 200 const status: fs.Stats = fs.statSync(filePath); 201 if (status.isDirectory()) { 202 readFile(filePath, utFiles); 203 } else { 204 utFiles.push(filePath); 205 } 206 }); 207 } catch (e) { 208 console.error(red, 'ArkTS ERROR: ' + e, reset); 209 } 210} 211 212export function circularFile(inputPath: string, outputPath: string): void { 213 if (!inputPath || !outputPath) { 214 return; 215 } 216 fs.readdir(inputPath, function(err, files) { 217 if (!files) { 218 return; 219 } 220 files.forEach(file => { 221 const inputFile: string = path.resolve(inputPath, file); 222 const outputFile: string = path.resolve(outputPath, file); 223 const fileStat: fs.Stats = fs.statSync(inputFile); 224 if (fileStat.isFile()) { 225 copyFile(inputFile, outputFile); 226 } else { 227 circularFile(inputFile, outputFile); 228 } 229 }); 230 }); 231} 232 233function copyFile(inputFile: string, outputFile: string): void { 234 try { 235 const parent: string = path.join(outputFile, '..'); 236 if (!(fs.existsSync(parent) && fs.statSync(parent).isDirectory())) { 237 mkDir(parent); 238 } 239 if (fs.existsSync(outputFile)) { 240 return; 241 } 242 const readStream: fs.ReadStream = fs.createReadStream(inputFile); 243 const writeStream: fs.WriteStream = fs.createWriteStream(outputFile); 244 readStream.pipe(writeStream); 245 readStream.on('close', function() { 246 writeStream.end(); 247 }); 248 } catch (err) { 249 throw err.message; 250 } 251} 252 253export function mkDir(path_: string): void { 254 const parent: string = path.join(path_, '..'); 255 if (!(fs.existsSync(parent) && !fs.statSync(parent).isFile())) { 256 mkDir(parent); 257 } 258 fs.mkdirSync(path_); 259} 260 261export function toUnixPath(data: string): string { 262 if (/^win/.test(require('os').platform())) { 263 const fileTmps: string[] = data.split(path.sep); 264 const newData: string = path.posix.join(...fileTmps); 265 return newData; 266 } 267 return data; 268} 269 270export function toHashData(path: string): any { 271 const content: string = fs.readFileSync(path).toString(); 272 const hash: any = createHash('sha256'); 273 hash.update(content); 274 return hash.digest('hex'); 275} 276 277export function writeFileSync(filePath: string, content: string): void { 278 if (!fs.existsSync(filePath)) { 279 const parent: string = path.join(filePath, '..'); 280 if (!(fs.existsSync(parent) && !fs.statSync(parent).isFile())) { 281 mkDir(parent); 282 } 283 } 284 fs.writeFileSync(filePath, content); 285} 286 287export function genTemporaryPath(filePath: string, projectPath: string, buildPath: string, 288 projectConfig: any, buildInHar: boolean = false): string { 289 filePath = toUnixPath(filePath).replace(/\.[cm]js$/, EXTNAME_JS); 290 projectPath = toUnixPath(projectPath); 291 292 if (process.env.compileTool === 'rollup') { 293 const projectRootPath: string = toUnixPath(buildInHar ? projectPath : projectConfig.projectRootPath); 294 const relativeFilePath: string = filePath.replace(projectRootPath, ''); 295 const output: string = path.join(buildPath, relativeFilePath); 296 return output; 297 } 298 299 if (isPackageModulesFile(filePath, projectConfig)) { 300 const packageDir: string = projectConfig.packageDir; 301 const fakePkgModulesPath: string = toUnixPath(path.join(projectConfig.projectRootPath, packageDir)); 302 let output: string = ''; 303 if (filePath.indexOf(fakePkgModulesPath) === -1) { 304 const hapPath: string = toUnixPath(projectConfig.projectRootPath); 305 const tempFilePath: string = filePath.replace(hapPath, ''); 306 const relativeFilePath: string = tempFilePath.substring(tempFilePath.indexOf(packageDir) + packageDir.length + 1); 307 output = path.join(buildPath, buildInHar ? '' : TEMPORARY, packageDir, MAIN, relativeFilePath); 308 } else { 309 output = filePath.replace(fakePkgModulesPath, 310 path.join(buildPath, buildInHar ? '' : TEMPORARY, packageDir, AUXILIARY)); 311 } 312 return output; 313 } 314 315 if (filePath.indexOf(projectPath) !== -1) { 316 const relativeFilePath: string = filePath.replace(projectPath, ''); 317 const output: string = path.join(buildPath, buildInHar ? '' : TEMPORARY, relativeFilePath); 318 return output; 319 } 320 321 return ''; 322} 323 324export function isPackageModulesFile(filePath: string, projectConfig: any): boolean { 325 filePath = toUnixPath(filePath); 326 const hapPath: string = toUnixPath(projectConfig.projectRootPath); 327 const tempFilePath: string = filePath.replace(hapPath, ''); 328 const packageDir: string = projectConfig.packageDir; 329 if (tempFilePath.indexOf(packageDir) !== -1) { 330 const fakePkgModulesPath: string = toUnixPath(path.resolve(projectConfig.projectRootPath, packageDir)); 331 if (filePath.indexOf(fakePkgModulesPath) !== -1) { 332 return true; 333 } 334 if (projectConfig.modulePathMap) { 335 for (const key in projectConfig.modulePathMap) { 336 const value: string = projectConfig.modulePathMap[key]; 337 const fakeModulePkgModulesPath: string = toUnixPath(path.resolve(value, packageDir)); 338 if (filePath.indexOf(fakeModulePkgModulesPath) !== -1) { 339 return true; 340 } 341 } 342 } 343 } 344 345 return false; 346} 347 348export function mkdirsSync(dirname: string): boolean { 349 if (fs.existsSync(dirname)) { 350 return true; 351 } else if (mkdirsSync(path.dirname(dirname))) { 352 fs.mkdirSync(dirname); 353 return true; 354 } 355 356 return false; 357} 358 359export function generateSourceFilesInHar(sourcePath: string, sourceContent: string, suffix: string, projectConfig: any) { 360 // compileShared: compile shared har of project 361 let jsFilePath: string = genTemporaryPath(sourcePath, 362 projectConfig.compileShared ? projectConfig.projectRootPath : projectConfig.moduleRootPath, 363 projectConfig.compileShared ? path.resolve(projectConfig.aceModuleBuild, '../etsFortgz') : projectConfig.cachePath, 364 projectConfig, projectConfig.compileShared); 365 if (!jsFilePath.match(new RegExp(projectConfig.packageDir))) { 366 jsFilePath = jsFilePath.replace(/\.ets$/, suffix).replace(/\.ts$/, suffix); 367 mkdirsSync(path.dirname(jsFilePath)); 368 if (projectConfig.obfuscateHarType === 'uglify' && suffix === '.js') { 369 sourceContent = uglifyJS.minify(sourceContent).code; 370 } 371 fs.writeFileSync(jsFilePath, sourceContent); 372 } 373} 374 375export function nodeLargeOrEqualTargetVersion(targetVersion: number): boolean { 376 const currentNodeVersion: number = parseInt(process.versions.node.split('.')[0]); 377 if (currentNodeVersion >= targetVersion) { 378 return true; 379 } 380 381 return false; 382} 383 384export function removeDir(dirName: string): void { 385 if (fs.existsSync(dirName)) { 386 if (nodeLargeOrEqualTargetVersion(16)) { 387 fs.rmSync(dirName, { recursive: true}); 388 } else { 389 fs.rmdirSync(dirName, { recursive: true}); 390 } 391 } 392} 393 394export function parseErrorMessage(message: string): string { 395 const messageArrary: string[] = message.split('\n'); 396 let logContent: string = ''; 397 messageArrary.forEach(element => { 398 if (!(/^at/.test(element.trim()))) { 399 logContent = logContent + element + '\n'; 400 } 401 }); 402 return logContent; 403} 404 405export function isWindows(): boolean { 406 return os.type() === WINDOWS; 407} 408 409export function isLinux(): boolean { 410 return os.type() === LINUX; 411} 412 413export function isMac(): boolean { 414 return os.type() === MAC; 415} 416 417export function maxFilePathLength(): number { 418 if (isWindows()) { 419 return 32766; 420 } else if (isLinux()) { 421 return 4095; 422 } else if (isMac()) { 423 return 1016; 424 } else { 425 return -1; 426 } 427} 428 429export function validateFilePathLength(filePath: string, logger: any): boolean { 430 if (maxFilePathLength() < 0) { 431 logger.error(red, "Unknown OS platform", reset); 432 process.exitCode = FAIL; 433 return false; 434 } else if (filePath.length > 0 && filePath.length <= maxFilePathLength()) { 435 return true; 436 } else if (filePath.length > maxFilePathLength()) { 437 logger.error(red, `The length of ${filePath} exceeds the limitation of current platform, which is ` + 438 `${maxFilePathLength()}. Please try moving the project folder to avoid deeply nested file path and try again`, 439 reset); 440 process.exitCode = FAIL; 441 return false; 442 } else { 443 logger.error(red, "Validate file path failed", reset); 444 process.exitCode = FAIL; 445 return false; 446 } 447} 448 449export function validateFilePathLengths(filePaths: Array<string>, logger: any): boolean { 450 filePaths.forEach((filePath) => { 451 if (!validateFilePathLength(filePath, logger)) { 452 return false; 453 } 454 }) 455 return true; 456} 457 458export function unlinkSync(filePath: string): void { 459 if (fs.existsSync(filePath)) { 460 fs.unlinkSync(filePath); 461 } 462} 463 464export function getExtensionIfUnfullySpecifiedFilepath(filePath: string): string { 465 if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { 466 return ""; 467 } 468 469 let extension: string = EXTNAME_ETS; 470 if (fs.existsSync(filePath + '.ts') && fs.statSync(filePath + '.ts').isFile()) { 471 extension = '.ts'; 472 } else if (fs.existsSync(filePath + '.d.ts') && fs.statSync(filePath + '.d.ts').isFile()) { 473 extension = '.d.ts'; 474 } else if (fs.existsSync(filePath + '.d.ets') && fs.statSync(filePath + '.d.ets').isFile()) { 475 extension = '.d.ets'; 476 } else if (fs.existsSync(filePath + '.js') && fs.statSync(filePath + '.js').isFile()) { 477 extension = '.js'; 478 } else if (fs.existsSync(filePath + '.json') && fs.statSync(filePath + '.json').isFile()) { 479 extension = '.json'; 480 } 481 482 return extension; 483} 484 485export function shouldWriteChangedList(watchModifiedFiles: string[], 486 watchRemovedFiles: string[]): boolean { 487 if (projectConfig.compileMode === ESMODULE && process.env.watchMode === 'true' && !projectConfig.isPreview && 488 projectConfig.changedFileList && (watchRemovedFiles.length + watchModifiedFiles.length)) { 489 if (process.env.compileTool !== 'rollup') { 490 if (!(watchModifiedFiles.length === 1 && 491 watchModifiedFiles[0] === projectConfig.projectPath && !watchRemovedFiles.length)) { 492 return true; 493 } else { 494 return false; 495 } 496 } 497 return true; 498 } 499 return false; 500} 501 502interface HotReloadIncrementalTime { 503 hotReloadIncrementalStartTime: string; 504 hotReloadIncrementalEndTime: string; 505} 506 507export const hotReloadIncrementalTime: HotReloadIncrementalTime = { 508 hotReloadIncrementalStartTime: '', 509 hotReloadIncrementalEndTime: '' 510}; 511 512interface FilesObj { 513 modifiedFiles: string[], 514 removedFiles: string[] 515} 516 517let allModifiedFiles: Set<string> = new Set(); 518 519export function getHotReloadFiles(watchModifiedFiles: string[], 520 watchRemovedFiles: string[], hotReloadSupportFiles: Set<string>): FilesObj { 521 hotReloadIncrementalTime.hotReloadIncrementalStartTime = new Date().getTime().toString(); 522 watchRemovedFiles = watchRemovedFiles.map(file => path.relative(projectConfig.projectPath, file)); 523 allModifiedFiles = new Set([...allModifiedFiles, ...watchModifiedFiles 524 .filter(file => fs.statSync(file).isFile() && 525 (hotReloadSupportFiles.has(file) || !['.ets', '.ts', '.js'].includes(path.extname(file)))) 526 .map(file => path.relative(projectConfig.projectPath, file))] 527 .filter(file => !watchRemovedFiles.includes(file))); 528 return { 529 modifiedFiles: [...allModifiedFiles], 530 removedFiles: [...watchRemovedFiles] 531 }; 532} 533 534export function getResolveModules(projectPath: string, faMode: boolean): string[] { 535 if (faMode) { 536 return [ 537 path.resolve(projectPath, '../../../../../'), 538 path.resolve(projectPath, '../../../../' + projectConfig.packageDir), 539 path.resolve(projectPath, '../../../../../' + projectConfig.packageDir), 540 path.resolve(projectPath, '../../') 541 ]; 542 } else { 543 return [ 544 path.resolve(projectPath, '../../../../'), 545 path.resolve(projectPath, '../../../' + projectConfig.packageDir), 546 path.resolve(projectPath, '../../../../' + projectConfig.packageDir), 547 path.resolve(projectPath, '../') 548 ]; 549 } 550} 551 552export function writeUseOSFiles(useOSFiles: Set<string>): void { 553 let info: string = ''; 554 if (!fs.existsSync(projectConfig.aceSoPath)) { 555 const parent: string = path.resolve(projectConfig.aceSoPath, '..'); 556 if (!(fs.existsSync(parent) && !fs.statSync(parent).isFile())) { 557 mkDir(parent); 558 } 559 } else { 560 info = fs.readFileSync(projectConfig.aceSoPath, 'utf-8') + '\n'; 561 } 562 fs.writeFileSync(projectConfig.aceSoPath, info + Array.from(useOSFiles).join('\n')); 563} 564 565export function getPossibleBuilderTypeParameter(parameters: ts.ParameterDeclaration[]): string[] { 566 const parameterNames: string[] = []; 567 if (!partialUpdateConfig.builderCheck) { 568 if (is$$Parameter(parameters)) { 569 parameters[0].type.members.forEach((member) => { 570 if (member.name && ts.isIdentifier(member.name)) { 571 parameterNames.push(member.name.escapedText.toString()); 572 } 573 }); 574 } else { 575 parameters.forEach((parameter) => { 576 if (parameter.name && ts.isIdentifier(parameter.name)) { 577 parameterNames.push(parameter.name.escapedText.toString()); 578 } 579 }); 580 } 581 } 582 return parameterNames; 583} 584 585function is$$Parameter(parameters: ts.ParameterDeclaration[]): boolean { 586 return parameters.length === 1 && parameters[0].name && ts.isIdentifier(parameters[0].name) && 587 parameters[0].name.escapedText.toString() === $$ && parameters[0].type && ts.isTypeLiteralNode(parameters[0].type) && 588 parameters[0].type.members && parameters[0].type.members.length > 0; 589} 590 591// Global Information 592class ProcessFileInfo { 593 buildStart: boolean = true; 594 wholeFileInfo: {[id: string]: SpecialArkTSFileInfo} = {}; // Saved ArkTS file's infomation 595 transformedFiles: string[] = []; // ArkTS Files which should be transformed in this compilation 596 cachedFiles: string[] = []; // ArkTS Files which should not be transformed in this compilation 597 shouldHaveEntry: string[] = []; // Which file should have @Entry decorator 598 599 addFileCacheInfo(id: string, fileCacheInfo: fileInfo): void { 600 if (id.match(/(?<!\.d)\.(ets)$/)) { 601 this.wholeFileInfo[id] = new SpecialArkTSFileInfo(fileCacheInfo); 602 } 603 } 604 605 collectTransformedFiles(id: string): void { 606 if (id.match(/(?<!\.d)\.(ets)$/)) { 607 this.transformedFiles.push(id); 608 } 609 } 610 611 collectCachedFiles(id: string): void { 612 if (id.match(/(?<!\.d)\.(ets)$/)) { 613 this.cachedFiles.push(id); 614 } 615 } 616 617 judgeShouldHaveEntryFiles(entryFileWithoutEntryDecorator: string[]): void { 618 this.shouldHaveEntry = Object.values(projectConfig.entryObj as string[]).filter((item) => { 619 return !entryFileWithoutEntryDecorator.includes(item.toLowerCase()) && item.match(/(?<!\.d)\.(ets)$/); 620 }); 621 } 622 623 saveCacheFileInfo(cache) { 624 const cacheInfo: {[id: string]: fileInfo} = cache.get('fileCacheInfo') || {}; 625 for (const id of this.transformedFiles) { 626 cacheInfo[id] = this.wholeFileInfo[id].fileInfo; 627 } 628 cache.set('fileCacheInfo', cacheInfo); 629 } 630} 631 632export const storedFileInfo: ProcessFileInfo = new ProcessFileInfo(); 633 634export interface fileInfo { 635 hasEntry: boolean; // Has @Entry decorator or not 636} 637 638// Save single file information 639class SpecialArkTSFileInfo { 640 fileInfo: fileInfo = { 641 hasEntry: false 642 }; 643 constructor(cacheInfo: fileInfo) { 644 this.fileInfo = cacheInfo || this.fileInfo; 645 } 646 get hasEntry() { 647 return this.fileInfo.hasEntry; 648 } 649 set hasEntry(value: boolean) { 650 this.fileInfo.hasEntry = value; 651 } 652} 653