• 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 ts from 'typescript';
17import fs from 'fs';
18import path from 'path';
19import { SourceMapGenerator } from 'source-map';
20
21import {
22  validateUISyntax,
23  componentCollection,
24  ReplaceResult,
25  sourceReplace
26} from './validate_ui_syntax';
27import {
28  LogType,
29  LogInfo,
30  mkDir,
31  emitLogInfo
32} from './utils';
33import {
34  MODULE_ETS_PATH,
35  MODULE_VISUAL_PATH,
36  SUPERVISUAL,
37  SUPERVISUAL_SOURCEMAP_EXT
38} from './pre_define';
39
40import { projectConfig } from '../main.js';
41import { genETS } from '../codegen/codegen_ets.js';
42import { concatenateEtsOptions, getExternalComponentPaths } from './external_component_map';
43
44const visualMap: Map<number, number> = new Map();
45const slotMap: Map<number, number> = new Map();
46
47const red: string = '\u001b[31m';
48const reset: string = '\u001b[39m';
49
50let compilerOptions = ts.readConfigFile(
51  path.resolve(__dirname, '../tsconfig.json'), ts.sys.readFile).config.compilerOptions;
52const componentPaths: string[] | undefined = getExternalComponentPaths();
53if (componentPaths) {
54  for (const componentPath of componentPaths) {
55    if (!fs.existsSync(componentPath)) {
56      continue;
57    }
58    const externalCompilerOptions: ts.CompilerOptions = ts.readConfigFile(
59      path.resolve(componentPath, 'externalconfig.json'), ts.sys.readFile
60    ).config.compilerOptions;
61    concatenateEtsOptions(compilerOptions, externalCompilerOptions);
62  }
63}
64compilerOptions.sourceMap = false;
65
66export function visualTransform(code: string, id: string, logger: any) {
67  const log: LogInfo[] = [];
68  const content: string | null = getParsedContent(code, path.normalize(id), log);
69  if (!content) {
70    return code;
71  }
72  if (log.length) {
73    emitLogInfo(logger, log, true, id);
74  }
75  generateSourceMapForNewAndOriEtsFile(path.normalize(id), code);
76  return content;
77}
78
79export function parseVisual(resourcePath: string, resourceQuery: string, content: string,
80  log: LogInfo[], source: string): string {
81  let code: string | null = getParsedContent(content, resourcePath, log);
82  if (!code) {
83    return content;
84  }
85  const result: ReplaceResult = sourceReplace(code, resourcePath);
86  code = result.content;
87  log.concat(result.log);
88  const resultLog: LogInfo[] = validateUISyntax(source, code, resourcePath, resourceQuery);
89  log.concat(resultLog);
90  if (!log.length) {
91    generateSourceMapForNewAndOriEtsFile(resourcePath, source);
92  }
93  return code;
94}
95
96function parseStatement(statement: ts.Statement, content: string, log: LogInfo[],
97  visualContent: any): string {
98  if (statement.kind === ts.SyntaxKind.StructDeclaration && statement.name) {
99    if (statement.members) {
100      statement.members.forEach(member => {
101        if (member.kind && member.kind === ts.SyntaxKind.MethodDeclaration) {
102          content = parseMember(statement, member, content, log, visualContent);
103        }
104      });
105    }
106  }
107  return content;
108}
109
110function parseMember(statement: ts.Statement, member: ts.MethodDeclaration, content: string,
111  log: LogInfo[], visualContent: any): string {
112  let newContent: string = content;
113  if (member.name && member.name.getText() === 'build') {
114    const buildBody: string = member.getText();
115    if (buildBody.replace(/\ +/g, '').replace(/[\r\n]/g, '') === 'build(){}') {
116      newContent = insertVisualCode(statement, member, visualContent, newContent);
117    } else {
118      log.push({
119        type: LogType.ERROR,
120        message: `when the corresponding visual file exists,` +
121          ` the build function of the entry component must be empty.`,
122        pos: member.pos
123      });
124    }
125  }
126  return newContent;
127}
128
129function insertVisualCode(statement: ts.Statement, member: ts.MethodDeclaration,
130  visualContent: any, content: string): string {
131  let newContent: string = content;
132  newContent = insertImport(visualContent, newContent);
133  newContent = insertVarAndFunc(member, visualContent, newContent, content);
134  newContent = insertBuild(member, visualContent, newContent, content);
135  newContent = insertAboutToAppear(statement, member, visualContent, newContent, content);
136  return newContent;
137}
138
139function insertImport(visualContent: any, content: string): string {
140  if (!visualContent.etsImport) {
141    return content;
142  }
143  const mediaQueryImport: string = visualContent.etsImport + '\n';
144  const newContent: string = mediaQueryImport + content;
145  slotMap.set(0, mediaQueryImport.length);
146  visualMap.set(0, mediaQueryImport.split('\n').length - 1);
147  return newContent;
148}
149
150function insertVarAndFunc(build: ts.MethodDeclaration, visualContent: any,
151  content: string, oriContent: string): string {
152  const visualVarAndFunc: string = (visualContent.etsVariable ? visualContent.etsVariable : '') +
153    (visualContent.etsFunction ? visualContent.etsFunction : '');
154  return visualVarAndFunc ? insertVisualCodeBeforePos(build, '\n' + visualVarAndFunc, content,
155    oriContent) : content;
156}
157
158function insertBuild(build: ts.MethodDeclaration, visualContent: any, content: string,
159  oriContent: string): string {
160  return visualContent.build ? insertVisualCodeAfterPos(build.body,
161    '\n' + visualContent.build + '\n', content, oriContent) : content;
162}
163
164function insertAboutToAppear(statement: ts.Statement, build: ts.MethodDeclaration,
165  visualContent: any, content: string, oriContent: string): string {
166  if (!visualContent.aboutToAppear) {
167    return content;
168  }
169  for (const member of statement.members) {
170    const hasAboutToAppear: boolean = member.kind && member.kind === ts.SyntaxKind.MethodDeclaration &&
171      member.name && member.name.getText() === 'aboutToAppear';
172    if (hasAboutToAppear) {
173      return insertVisualCodeAfterPos(member.body, '\n' + visualContent.aboutToAppear, content,
174        oriContent);
175    }
176  }
177
178  const aboutToAppearFunc: string = '\n  aboutToAppear() {\n' + visualContent.aboutToAppear +
179    '  }\n';
180  return insertVisualCodeBeforePos(build, aboutToAppearFunc, content, oriContent);
181}
182
183function insertVisualCodeAfterPos(member: ts.Block, visualContent: string, content: string,
184  oriContent: string): string {
185  const contentBeforePos: string = oriContent.substring(0, member.getStart() + 1);
186  const originEtsFileLineNumber: number = contentBeforePos.split('\n').length;
187  const visualLines: number = visualContent.split('\n').length - 1;
188  const insertedLineNumbers: number = visualMap.get(originEtsFileLineNumber);
189  visualMap.set(originEtsFileLineNumber, insertedLineNumbers ? insertedLineNumbers + visualLines :
190    visualLines);
191
192  let newPos: number = member.getStart() + 1;
193  for (const [key, value] of slotMap) {
194    if (member.getStart() >= key) {
195      newPos += value;
196    }
197  }
198
199  const newContent: string = content.substring(0, newPos) + visualContent +
200    content.substring(newPos);
201  slotMap.set(member.getStart(), visualContent.length);
202  return newContent;
203}
204
205function insertVisualCodeBeforePos(member: ts.MethodDeclaration, visualContent: string,
206  content: string, oriContent: string): string {
207  const contentBeforePos: string = oriContent.substring(0, member.pos);
208  const originEtsFileLineNumber: number = contentBeforePos.split('\n').length;
209  const visualLines: number = visualContent.split('\n').length - 1;
210  const insertedLineNumbers: number = visualMap.get(originEtsFileLineNumber);
211  visualMap.set(originEtsFileLineNumber, insertedLineNumbers ? insertedLineNumbers + visualLines :
212    visualLines);
213  let newPos: number = member.pos;
214  for (const [key, value] of slotMap) {
215    if (member.pos >= key) {
216      newPos += value;
217    }
218  }
219  const newContent: string = content.substring(0, newPos) + visualContent +
220    content.substring(newPos);
221  slotMap.set(member.pos, visualContent.length);
222  return newContent;
223}
224
225function generateSourceMapForNewAndOriEtsFile(resourcePath: string, content: string) {
226  if (!process.env.cachePath) {
227    return;
228  }
229  const sourcemap: SourceMapGenerator = new SourceMapGenerator({
230    file: resourcePath
231  });
232  const lines: Array<string> = content.split('\n');
233  const originEtsFileLines: number = lines.length;
234  for (let l: number = 1; l <= originEtsFileLines; l++) {
235    let newEtsFileLineNumber: number = l;
236    for (const [originEtsFileLineNumber, visualLines] of visualMap) {
237      if (l > originEtsFileLineNumber) {
238        newEtsFileLineNumber += visualLines;
239      }
240    }
241    sourcemap.addMapping({
242      generated: {
243        line: newEtsFileLineNumber,
244        column: 0
245      },
246      source: resourcePath,
247      original: {
248        line: l,
249        column: 0
250      }
251    });
252  }
253  const visualMapName: string = path.parse(resourcePath).name + SUPERVISUAL_SOURCEMAP_EXT;
254  const visualDirPath: string = path.parse(resourcePath).dir;
255  const etsDirPath: string = path.parse(projectConfig.projectPath).dir;
256  let visualMapDirPath: string = path.resolve(process.env.cachePath, SUPERVISUAL +
257    visualDirPath.replace(etsDirPath, ''));
258  if (!visualDirPath.includes(etsDirPath)) {
259    const projectRootPath = getProjectRootPath();
260    visualMapDirPath = path.resolve(process.env.cachePath, SUPERVISUAL +
261      visualDirPath.replace(projectRootPath, ''));
262  }
263  if (!(fs.existsSync(visualMapDirPath) && fs.statSync(visualMapDirPath).isDirectory())) {
264    mkDir(visualMapDirPath);
265  }
266  fs.writeFile(path.resolve(visualMapDirPath, visualMapName), sourcemap.toString(), (err) => {
267    if (err) {
268      console.error(red, 'ERROR: Failed to write visual.js.map', reset);
269    }
270  });
271}
272
273function getProjectRootPath(): string {
274  let projectRootPath = projectConfig.projectRootPath;
275  if (!projectRootPath) {
276    if (!projectConfig.aceModuleJsonPath) {
277      projectRootPath = path.resolve(projectConfig.projectPath, '../../../../../');
278    } else {
279      projectRootPath = path.resolve(projectConfig.projectPath, '../../../../');
280    }
281  }
282  return projectRootPath;
283}
284
285export function findVisualFile(filePath: string): string {
286  if (!/\.ets$/.test(filePath)) {
287    return '';
288  }
289  let etsDirPath: string = path.parse(projectConfig.projectPath).dir;
290  let visualDirPath: string = path.parse(projectConfig.aceSuperVisualPath).dir;
291  let resolvePath = filePath.replace(projectConfig.projectPath, projectConfig.aceSuperVisualPath)
292    .replace(etsDirPath, visualDirPath).replace(/\.ets$/, '.visual');
293  if (fs.existsSync(resolvePath)) {
294    return resolvePath;
295  }
296  try {
297    const projectRootPath = getProjectRootPath();
298    let moduleName = '';
299    const relativePath = filePath.replace(projectRootPath, '');
300    const moduleNames = relativePath.split(path.sep);
301    for (let i = 0; i < moduleNames.length; ++i) {
302      if (moduleNames[i] === 'src') {
303        if (i >= moduleNames.length - 2) {
304          break;
305        }
306        const modulePath = path.join(moduleNames[i], moduleNames[i + 1], moduleNames[i + 2]);
307        if (modulePath === MODULE_ETS_PATH) {
308          break;
309        }
310      }
311      moduleName = path.join(moduleName, moduleNames[i]);
312    }
313    etsDirPath = path.join(projectRootPath, moduleName, MODULE_ETS_PATH);
314    visualDirPath = path.join(projectRootPath, moduleName, MODULE_VISUAL_PATH);
315    resolvePath = filePath.replace(etsDirPath, visualDirPath).replace(/\.ets$/, '.visual');
316    return resolvePath;
317  } catch (e) {
318    // avoid projectConfig attributes has undefined value
319    return '';
320  }
321}
322
323function getVisualContent(visualPath: string, log: LogInfo[]): any {
324  const parseContent: any = genETS(fs.readFileSync(visualPath, 'utf-8'));
325  if (parseContent && parseContent.errorType && parseContent.errorType !== '') {
326    log.push({
327      type: LogType.ERROR,
328      message: parseContent.errorMessage
329    });
330  }
331  return parseContent ? parseContent.ets : null;
332}
333
334function getParsedContent(code: string, id: string, log: LogInfo[]): string | null {
335  if (!projectConfig.aceSuperVisualPath ||
336    !(componentCollection.entryComponent || componentCollection.customComponents)) {
337      return null;
338  }
339  const visualPath: string = findVisualFile(id);
340  if (!visualPath || !fs.existsSync(visualPath)) {
341    return null;
342  }
343  const visualContent: any = getVisualContent(visualPath, log);
344  if (!visualContent) {
345    return null;
346  }
347  clearVisualSlotMap();
348  const sourceFile: ts.SourceFile = ts.createSourceFile(
349    id,
350    code,
351    ts.ScriptTarget.Latest,
352    true,
353    ts.ScriptKind.ETS,
354    compilerOptions
355  );
356  let content: string = code;
357  if (sourceFile.statements) {
358    sourceFile.statements.forEach(statement => {
359      content = parseStatement(statement, content, log, visualContent);
360    });
361  }
362  return content;
363}
364
365function clearVisualSlotMap(): void {
366  visualMap.clear();
367  slotMap.clear();
368}