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