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