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