1// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import { parse, print, types, visit } from 'recast';
6import fs from 'fs';
7import path from 'path';
8import { promisify } from 'util';
9import { getMappings } from './get-mappings.js';
10
11const readDir = promisify(fs.readdir);
12const readFile = promisify(fs.readFile);
13const writeFile = promisify(fs.writeFile);
14const b = types.builders;
15
16const FRONT_END_FOLDER = path.join(__dirname, '..', '..', 'front_end');
17
18async function rewriteSource(pathName: string, srcFile: string, mappings:Map<string, any>, useExternalRefs = false) {
19  const filePath = path.join(pathName, srcFile);
20  const srcFileContents = await readFile(filePath, { encoding: 'utf-8' });
21  const ast = parse(srcFileContents);
22
23  const importsRequired = new Set<{file: string, replacement: string, sameFolderReplacement: string}>();
24
25  visit(ast, {
26    visitComment(path) {
27      const comments = (path.node as any).comments;
28      for (const comment of comments) {
29
30        if (comment.loc) {
31          (comment.loc as any).indent = 0;
32        }
33
34        for (const [str, value] of mappings.entries()) {
35          const containsString = new RegExp(`${str}([^\\.\\w])`, 'g');
36          const stringMatches = containsString.exec(comment.value);
37
38          if (!stringMatches) {
39            continue;
40          }
41
42          const replacement = useExternalRefs ? value.replacement : value.sameFolderReplacement;
43
44          importsRequired.add(value);
45          comment.value = comment.value.replace(stringMatches[0], replacement + stringMatches[0].slice(-1));
46        }
47      }
48
49      this.traverse(path);
50    },
51
52    visitMemberExpression(path) {
53      const node = path.node;
54      const nodeCopy = b.memberExpression.from({...node, comments: []});
55      const nodeAsCode = print(nodeCopy).code;
56
57      for (const [str, value] of mappings.entries()) {
58        if (nodeAsCode !== str) {
59          continue;
60        }
61
62        const name = useExternalRefs ? value.replacement : value.sameFolderReplacement;
63
64        importsRequired.add(value);
65        return b.identifier.from({ name, comments: node.comments || [] });
66      }
67
68      this.traverse(path);
69    },
70  });
71
72  const importMap = new Map<string, any[]>();
73  for (const { file, sameFolderReplacement, replacement } of importsRequired) {
74    if (filePath === file) {
75      continue;
76    }
77
78    const src = path.dirname(filePath);
79    const dst = path.dirname(file);
80
81    let replacementIdentifier = '';
82    let relativePath = path.relative(src, dst);
83    const isSameFolder = relativePath === '';
84    if (isSameFolder) {
85      relativePath = './';
86      replacementIdentifier = sameFolderReplacement;
87    } else {
88      relativePath += '/';
89      replacementIdentifier = replacement;
90    }
91
92    const targetImportFile = relativePath + path.basename(file);
93
94    if (!importMap.has(targetImportFile)) {
95      importMap.set(targetImportFile, []);
96    }
97
98    const imports = importMap.get(targetImportFile)!;
99    if (useExternalRefs) {
100      if (imports.length === 0) {
101        // We are creating statements like import * as Foo from '../foo/foo.js' so
102        // here we take the first part of the identifier, e.g. Foo.Bar.Bar gives us
103        // Foo so we can make import * as Foo from that.
104        const namespaceIdentifier = replacementIdentifier.split('.')[0];
105        imports.push(b.importNamespaceSpecifier(b.identifier(namespaceIdentifier)));
106      }
107
108      // Make sure there is only one import * from Foo import added.
109      continue;
110    }
111
112    imports.push(b.importSpecifier(b.identifier(replacementIdentifier)));
113  }
114
115  // Add missing imports.
116  for (const [targetImportFile, specifiers] of importMap) {
117    const newImport = b.importDeclaration.from({
118      specifiers,
119      comments: ast.program.body[0].comments,
120      source: b.literal(targetImportFile),
121    });
122
123    // Remove any file comments.
124    ast.program.body[0].comments = [];
125
126    // Add the import statements.
127    ast.program.body.unshift(newImport);
128  }
129
130  return print(ast).code;
131}
132
133async function main(folder: string, namespaces?: string[]) {
134  const pathName = path.join(FRONT_END_FOLDER, folder);
135  const srcDir = await readDir(pathName);
136  const useExternalRefs = namespaces !== undefined && (namespaces[0] !== folder);
137  let mappings = new Map();
138  if (namespaces && namespaces.length) {
139    for (const namespace of namespaces) {
140      mappings = await getMappings(namespace, mappings, useExternalRefs);
141    }
142  } else {
143    mappings = await getMappings(folder, mappings, useExternalRefs);
144  }
145
146  for (const srcFile of srcDir) {
147    if (srcFile === `${folder}.js` || srcFile === `${folder}-legacy.js` || !srcFile.endsWith('.js')) {
148      continue;
149    }
150
151    const distFileContents = await rewriteSource(pathName, srcFile, mappings, useExternalRefs);
152    await writeFile(path.join(pathName, `${srcFile}`), distFileContents);
153  }
154}
155
156if (!process.argv[2]) {
157  console.error('No arguments specified. Run this script with "<folder-name>". For example: "ui"');
158  process.exit(1);
159}
160
161main(process.argv[2], process.argv[3] && process.argv[3].split(',') || undefined);
162