blob: 14a80e3b8443188521e4a26b87a800c504351392 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
const fs = require('fs');
const path = require('path');
const utils = require('../utils');
const FRONTEND_PATH = path.resolve(__dirname, '..', '..', 'front_end');
const BUILD_GN_PATH = path.resolve(__dirname, '..', '..', 'BUILD.gn');
const SPECIAL_CASE_NAMESPACES_PATH = path.resolve(__dirname, '..', 'special_case_namespaces.json');
/*
* This is used to extract a new module from an existing module by:
* - Moving selected files into new modules (including relevant
* css files)
* - Renaming all identifiers to the new namespace
* - Updating the BUILD.gn and module.json files (including extensions)
* ==========================================
* START EDITING HERE - TRANSFORMATION INPUTS
* ==========================================
*/
const APPLICATION_DESCRIPTORS = [
'devtools_app.json',
'js_app.json',
'shell.json',
'worker_app.json',
'inspector.json',
'toolbox.json',
'integration_test_runner.json',
'formatter_worker.json',
'heap_snapshot_worker.json',
];
/*
* If the transformation removes all the files of a module:
* ['text_editor']
*/
const MODULES_TO_REMOVE = ['profiler_test_runner', 'heap_snapshot_test_runner'];
/**
* If moving to a new module:
* {file: 'common/Text.js', new: 'a_new_module'}
*
* If moving to an existing module:
* {file: 'ui/SomeFile.js', existing: 'common'}
*/
const JS_FILES_MAPPING = [
// {file: 'heap_snapshot_test_runner/HeapSnapshotTestRunner.js', new: 'heap_profiler_test_runner'},
// {file: 'profiler_test_runner/ProfilerTestRunner.js', new: 'cpu_profiler_test_runner'},
{file: 'network_log/HAREntry.js', existing: 'browser_sdk'},
];
/**
* List all new modules here:
* mobile_throttling: {
* dependencies: ['sdk'],
* dependents: ['console'],
* applications: ['inspector.json'],
* autostart: false,
* }
*/
const MODULE_MAPPING = {
// heap_profiler_test_runner: {
// dependencies: ['heap_snapshot_worker', 'test_runner'],
// dependents: [],
// applications: ['integration_test_runner.json'],
// autostart: false,
// },
// cpu_profiler_test_runner: {
// dependencies: ['profiler', 'test_runner'],
// dependents: [],
// applications: ['integration_test_runner.json'],
// autostart: false,
// },
};
/**
* If an existing module will have a new dependency on an existing module:
* console: ['new_dependency']
*/
const NEW_DEPENDENCIES_BY_EXISTING_MODULES = {
// resources: ['components'],
};
/**
* If an existing module will no longer have a dependency on a module:
* console: ['former_dependency']
*/
const REMOVE_DEPENDENCIES_BY_EXISTING_MODULES = {
// console_test_runner: ['main']
};
/*
* ==========================================
* STOP EDITING HERE
* ==========================================
*/
const DEPENDENCIES_BY_MODULE = Object.keys(MODULE_MAPPING).reduce((acc, module) => {
acc[module] = MODULE_MAPPING[module].dependencies;
return acc;
}, {});
const APPLICATIONS_BY_MODULE = Object.keys(MODULE_MAPPING).reduce((acc, module) => {
acc[module] = MODULE_MAPPING[module].applications;
return acc;
}, {});
const DEPENDENTS_BY_MODULE = Object.keys(MODULE_MAPPING).reduce((acc, module) => {
acc[module] = MODULE_MAPPING[module].dependents;
return acc;
}, {});
function extractModule() {
const modules = new Set();
for (let fileObj of JS_FILES_MAPPING) {
let moduleName = fileObj.file.split('/')[0];
modules.add(moduleName);
}
const newModuleSet = JS_FILES_MAPPING.reduce((acc, file) => file.new ? acc.add(file.new) : acc, new Set());
const targetToOriginalFilesMap = JS_FILES_MAPPING.reduce((acc, f) => {
let components = f.file.split('/');
components[0] = f.new || f.existing;
acc.set(components.join('/'), f.file);
return acc;
}, new Map());
const cssFilesMapping = findCSSFiles();
console.log('cssFilesMapping', cssFilesMapping);
const identifiersByFile = calculateIdentifiers();
const identifierMap = mapIdentifiers(identifiersByFile, cssFilesMapping);
console.log('identifierMap', identifierMap);
const extensionMap = removeFromExistingModuleDescriptors(modules, identifierMap, cssFilesMapping);
// Find out which files are moving extensions
for (let e of extensionMap.keys()) {
for (let [f, identifiers] of identifiersByFile) {
if (identifiers.includes(e))
console.log(`extension: ${e} in file: ${f}`);
}
}
moveFiles(cssFilesMapping);
createNewModuleDescriptors(extensionMap, cssFilesMapping, identifiersByFile, targetToOriginalFilesMap);
updateExistingModuleDescriptors(extensionMap, cssFilesMapping, identifiersByFile, targetToOriginalFilesMap);
addDependenciesToDescriptors();
renameIdentifiers(identifierMap);
updateBuildGNFile(cssFilesMapping, newModuleSet);
for (let descriptor of APPLICATION_DESCRIPTORS)
updateApplicationDescriptor(descriptor, newModuleSet);
for (let m of MODULES_TO_REMOVE)
utils.removeRecursive(path.resolve(FRONTEND_PATH, m));
}
String.prototype.replaceAll = function(search, replacement) {
let target = this;
return target.replace(new RegExp('\\b' + search + '\\b', 'g'), replacement);
};
Set.prototype.union = function(setB) {
let union = new Set(this);
for (let elem of setB)
union.add(elem);
return union;
};
function mapModuleToNamespace(module) {
const specialCases = require(SPECIAL_CASE_NAMESPACES_PATH);
return specialCases[module] || toCamelCase(module);
function toCamelCase(module) {
return module.split('_').map(a => a.substring(0, 1).toUpperCase() + a.substring(1)).join('');
}
}
function findCSSFiles() {
let cssFilesMapping = new Map();
for (let fileObj of JS_FILES_MAPPING)
cssFilesMapping.set(fileObj.file, scrapeCSSFile(fileObj.file));
function scrapeCSSFile(filePath) {
let cssFiles = new Set();
const fullPath = path.resolve(FRONTEND_PATH, filePath);
let content = fs.readFileSync(fullPath).toString();
let lines = content.split('\n');
for (let line of lines) {
let match = line.match(/'(.+\.css)'/);
if (!match)
continue;
let matchPath = match[1];
cssFiles.add(path.basename(path.resolve(FRONTEND_PATH, matchPath)));
}
return cssFiles;
}
return cssFilesMapping;
}
function calculateIdentifiers() {
const identifiersByFile = new Map();
for (let fileObj of JS_FILES_MAPPING) {
const fullPath = path.resolve(FRONTEND_PATH, fileObj.file);
let content = fs.readFileSync(fullPath).toString();
identifiersByFile.set(fileObj.file, scrapeIdentifiers(content, fileObj));
}
return identifiersByFile;
function scrapeIdentifiers(content, fileObj) {
let identifiers = [];
let lines = content.split('\n');
for (let line of lines) {
let match =
line.match(new RegExp(`^\\s*([a-z_A-Z0-9\.]+)\\s=`)) || line.match(new RegExp(`^\\s*([a-z_A-Z0-9\.]+);`));
if (!match)
continue;
let name = match[1];
var currentModule = fileObj.file.split('/')[0];
if (name.split('.')[0] !== mapModuleToNamespace(currentModule)) {
console.log(`POSSIBLE ISSUE: identifier: ${name} found in ${currentModule}`);
// one-off
if (name.includes('UI.')) {
console.log(`including ${name} anyways`);
identifiers.push(name);
}
} else {
identifiers.push(name);
}
}
return identifiers;
}
}
function moveFiles(cssFilesMapping) {
for (let fileObj of JS_FILES_MAPPING) {
let sourceFilePath = path.resolve(FRONTEND_PATH, fileObj.file);
let targetFilePath = getMappedFilePath(fileObj);
let moduleDir = path.resolve(targetFilePath, '..');
if (!fs.existsSync(moduleDir))
fs.mkdirSync(moduleDir);
move(sourceFilePath, targetFilePath);
if (cssFilesMapping.has(fileObj.file)) {
cssFilesMapping.get(fileObj.file).forEach((file) => {
let module = fileObj.new || fileObj.existing;
move(path.resolve(FRONTEND_PATH, fileObj.file.split('/')[0], file), path.resolve(FRONTEND_PATH, module, file));
});
}
}
function move(sourceFilePath, targetFilePath) {
try {
fs.writeFileSync(targetFilePath, fs.readFileSync(sourceFilePath));
fs.unlinkSync(sourceFilePath);
} catch (err) {
console.log(`error moving ${sourceFilePath} -> ${targetFilePath}`);
}
}
function getMappedFilePath(fileObj) {
let components = fileObj.file.split('/');
components[0] = fileObj.existing || fileObj.new;
return path.resolve(FRONTEND_PATH, components.join('/'));
}
}
function updateBuildGNFile(cssFilesMapping, newModuleSet) {
let content = fs.readFileSync(BUILD_GN_PATH).toString();
let newSourcesToAdd = [];
let partialPathMapping = calculatePartialPathMapping();
for (let module of MODULES_TO_REMOVE) {
partialPathMapping.set(`"front_end/${module}/module.json",\n`, '');
partialPathMapping.set(`"$resources_out_dir/${module}/${module}_module.js",\n`, '');
}
const newNonAutostartModules = [...newModuleSet]
.filter(module => !MODULE_MAPPING[module].autostart)
.map(module => `"$resources_out_dir/${module}/${module}_module.js",`);
let newContent = addContentToLinesInSortedOrder({
content,
startLine: 'generated_non_autostart_non_remote_modules = [',
endLine: ']',
linesToInsert: newNonAutostartModules,
});
for (let pair of partialPathMapping.entries())
newContent = newContent.replace(pair[0], pair[1]);
newContent = addContentToLinesInSortedOrder({
content: newContent,
startLine: 'all_devtools_files = [',
endLine: ']',
linesToInsert: newSourcesToAdd.concat([...newModuleSet].map(module => `"front_end/${module}/module.json",`)),
});
fs.writeFileSync(BUILD_GN_PATH, newContent);
function calculatePartialPathMapping() {
let partialPathMapping = new Map();
for (let fileObj of JS_FILES_MAPPING) {
let components = fileObj.file.split('/');
let sourceModule = components[0];
let targetModule = fileObj.existing || fileObj.new;
components[0] = targetModule;
partialPathMapping.set(`"front_end/${fileObj.file}",\n`, '');
newSourcesToAdd.push(`"front_end/${components.join('/')}",`);
if (cssFilesMapping.has(fileObj.file)) {
for (let cssFile of cssFilesMapping.get(fileObj.file)) {
partialPathMapping.set(`"front_end/${sourceModule}/${cssFile}",\n`, '');
newSourcesToAdd.push(`"front_end/${targetModule}/${cssFile}",`);
}
}
}
return partialPathMapping;
}
function top(array) {
return array[array.length - 1];
}
function addContentToLinesInSortedOrder({content, startLine, endLine, linesToInsert}) {
if (linesToInsert.length === 0)
return content;
let lines = content.split('\n');
let seenStartLine = false;
let contentStack = linesToInsert.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())).reverse();
for (var i = 0; i < lines.length; i++) {
let line = lines[i].trim();
let nextLine = lines[i + 1].trim();
if (line === startLine)
seenStartLine = true;
if (line === endLine && seenStartLine)
break;
if (!seenStartLine)
continue;
const nextContent = top(contentStack) ? top(contentStack).toLowerCase() : '';
if ((line === startLine || nextContent >= line.toLowerCase()) &&
(nextLine === endLine || nextContent <= nextLine.toLowerCase()))
lines.splice(i + 1, 0, contentStack.pop());
}
if (contentStack.length)
lines.splice(i, 0, ...contentStack);
return lines.join('\n');
}
}
function mapIdentifiers(identifiersByFile, cssFilesMapping) {
const filesToTargetModule = new Map();
for (let fileObj of JS_FILES_MAPPING)
filesToTargetModule.set(fileObj.file, fileObj.existing || fileObj.new);
const map = new Map();
for (let [file, identifiers] of identifiersByFile) {
let targetModule = filesToTargetModule.get(file);
for (let identifier of identifiers) {
let components = identifier.split('.');
components[0] = mapModuleToNamespace(targetModule);
let newIdentifier = components.join('.');
map.set(identifier, newIdentifier);
}
}
for (let [jsFile, cssFiles] of cssFilesMapping) {
let fileObj = JS_FILES_MAPPING.filter(f => f.file === jsFile)[0];
let sourceModule = fileObj.file.split('/')[0];
let targetModule = fileObj.existing || fileObj.new;
for (let cssFile of cssFiles) {
let key = `${sourceModule}/${cssFile}`;
let value = `${targetModule}/${cssFile}`;
map.set(key, value);
}
}
return map;
}
function renameIdentifiers(identifierMap) {
walkSync('front_end', write, true);
walkSync('../../web_tests/http/tests/devtools', write, false);
walkSync('../../web_tests/http/tests/inspector-protocol', write, false);
walkSync('../../web_tests/inspector-protocol', write, false);
function walkSync(currentDirPath, process, json) {
fs.readdirSync(currentDirPath).forEach(function(name) {
let filePath = path.join(currentDirPath, name);
let stat = fs.statSync(filePath);
if (stat.isFile() && (filePath.endsWith('.js') || filePath.endsWith('.html') || filePath.endsWith('.xhtml') ||
filePath.endsWith('-expected.txt') || (json && filePath.endsWith('.json')))) {
if (filePath.includes('ExtensionAPI.js'))
return;
if (filePath.includes('externs.js'))
return;
if (filePath.includes('eslint') || filePath.includes('lighthouse-dt-bundle.js') || filePath.includes('/cm/') ||
filePath.includes('/xterm.js/') || filePath.includes('/acorn/'))
return;
if (filePath.includes('/cm_modes/') && !filePath.includes('DefaultCodeMirror') &&
!filePath.includes('module.json'))
return;
process(filePath);
} else if (stat.isDirectory()) {
walkSync(filePath, process, json);
}
});
}
function write(filePath) {
let content = fs.readFileSync(filePath).toString();
let newContent = content;
for (let key of identifierMap.keys()) {
let originalIdentifier = key;
let newIdentifier = identifierMap.get(key);
newContent = newContent.replaceAll(originalIdentifier, newIdentifier);
}
if (content !== newContent)
fs.writeFileSync(filePath, newContent);
}
}
function removeFromExistingModuleDescriptors(modules, identifierMap, cssFilesMapping) {
let extensionMap = new Map();
let moduleFileMap = new Map();
for (let fileObj of JS_FILES_MAPPING) {
let components = fileObj.file.split('/');
let module = components[0];
let fileName = components[1];
if (!moduleFileMap.get(module))
moduleFileMap.set(module, []);
moduleFileMap.set(module, moduleFileMap.get(module).concat(fileName));
}
for (let module of modules) {
let moduleJSONPath = path.resolve(FRONTEND_PATH, module, 'module.json');
let content = fs.readFileSync(moduleJSONPath).toString();
let moduleObj = parseJSON(content);
let removedScripts = removeScripts(moduleObj, module);
removeResources(moduleObj, removedScripts);
removeExtensions(moduleObj);
fs.writeFileSync(moduleJSONPath, stringifyJSON(moduleObj));
}
return extensionMap;
function removeScripts(moduleObj, module) {
let remainingScripts = [];
let removedScripts = [];
let moduleFiles = moduleFileMap.get(module);
for (let script of moduleObj.scripts) {
if (!moduleFiles.includes(script))
remainingScripts.push(script);
else
removedScripts.push(module + '/' + script);
}
moduleObj.scripts = remainingScripts;
return removedScripts;
}
function removeResources(moduleObj, removedScripts) {
if (!moduleObj.resources)
return;
let remainingResources = [];
let removedResources = new Set();
for (let script of removedScripts)
removedResources = removedResources.union(cssFilesMapping.get(script));
for (let resource of moduleObj.resources) {
if (!removedResources.has(resource))
remainingResources.push(resource);
}
moduleObj.resources = remainingResources;
}
function removeExtensions(moduleObj) {
if (!moduleObj.extensions)
return;
let remainingExtensions = [];
for (let extension of moduleObj.extensions) {
if (!objectIncludesIdentifier(extension)) {
remainingExtensions.push(extension);
} else {
if (extensionMap.has(objectIncludesIdentifier(extension))) {
let existingExtensions = extensionMap.get(objectIncludesIdentifier(extension));
extensionMap.set(objectIncludesIdentifier(extension), existingExtensions.concat(extension));
} else {
extensionMap.set(objectIncludesIdentifier(extension), [extension]);
}
}
}
moduleObj.extensions = remainingExtensions;
}
function objectIncludesIdentifier(object) {
for (let key in object) {
let value = object[key];
if (identifierMap.has(value))
return value;
}
return false;
}
}
function createNewModuleDescriptors(extensionMap, cssFilesMapping, identifiersByFile, targetToOriginalFilesMap) {
let filesByNewModule = getFilesByNewModule();
for (let module of filesByNewModule.keys()) {
let moduleObj = {};
let scripts = getModuleScripts(module);
let extensions = getModuleExtensions(scripts, module);
if (extensions.length)
moduleObj.extensions = extensions;
moduleObj.dependencies = DEPENDENCIES_BY_MODULE[module];
moduleObj.scripts = scripts;
let resources = getModuleResources(moduleObj.scripts, module);
if (resources.length)
moduleObj.resources = resources;
let moduleJSONPath = path.resolve(FRONTEND_PATH, module, 'module.json');
fs.writeFileSync(moduleJSONPath, stringifyJSON(moduleObj));
}
function getFilesByNewModule() {
let filesByNewModule = new Map();
for (let fileObj of JS_FILES_MAPPING) {
if (!fileObj.new)
continue;
if (!filesByNewModule.has(fileObj.new))
filesByNewModule.set(fileObj.new, []);
filesByNewModule.set(fileObj.new, filesByNewModule.get(fileObj.new).concat([fileObj.file]));
}
return filesByNewModule;
}
function getModuleScripts(module) {
return filesByNewModule.get(module).map((file) => file.split('/')[1]);
}
function getModuleResources(scripts, module) {
let resources = [];
scripts.map(script => module + '/' + script).forEach((script) => {
script = targetToOriginalFilesMap.get(script);
if (!cssFilesMapping.has(script))
return;
resources = resources.concat([...cssFilesMapping.get(script)]);
});
return resources;
}
function getModuleExtensions(scripts, module) {
let extensions = [];
let identifiers =
scripts.map(script => module + '/' + script)
.reduce((acc, file) => acc.concat(identifiersByFile.get(targetToOriginalFilesMap.get(file))), []);
for (let identifier of identifiers) {
if (extensionMap.has(identifier))
extensions = extensions.concat(extensionMap.get(identifier));
}
return extensions;
}
}
function calculateFilesByModuleType(type) {
let filesByNewModule = new Map();
for (let fileObj of JS_FILES_MAPPING) {
if (!fileObj[type])
continue;
if (!filesByNewModule.has(fileObj[type]))
filesByNewModule.set(fileObj[type], []);
filesByNewModule.set(fileObj[type], filesByNewModule.get(fileObj[type]).concat([fileObj.file]));
}
return filesByNewModule;
}
function updateExistingModuleDescriptors(extensionMap, cssFilesMapping, identifiersByFile, targetToOriginalFilesMap) {
let filesByExistingModule = calculateFilesByModuleType('existing');
for (let module of filesByExistingModule.keys()) {
let moduleJSONPath = path.resolve(FRONTEND_PATH, module, 'module.json');
let content = fs.readFileSync(moduleJSONPath).toString();
let moduleObj = parseJSON(content);
let scripts = getModuleScripts(module);
let existingExtensions = moduleObj.extensions || [];
let extensions = existingExtensions.concat(getModuleExtensions(scripts, module));
if (extensions.length)
moduleObj.extensions = extensions;
moduleObj.scripts = moduleObj.scripts.concat(scripts);
let existingResources = moduleObj.resources || [];
let resources = existingResources.concat(getModuleResources(scripts, module));
if (resources.length)
moduleObj.resources = resources;
fs.writeFileSync(moduleJSONPath, stringifyJSON(moduleObj));
}
function getModuleScripts(module) {
return filesByExistingModule.get(module).map((file) => file.split('/')[1]);
}
function getModuleResources(scripts, module) {
let resources = [];
scripts.map(script => module + '/' + script).forEach((script) => {
script = targetToOriginalFilesMap.get(script);
if (!cssFilesMapping.has(script))
return;
resources = resources.concat([...cssFilesMapping.get(script)]);
});
return resources;
}
function getModuleExtensions(scripts, module) {
let extensions = [];
let identifiers =
scripts.map(script => module + '/' + script)
.reduce((acc, file) => acc.concat(identifiersByFile.get(targetToOriginalFilesMap.get(file))), []);
for (let identifier of identifiers) {
if (extensionMap.has(identifier))
extensions = extensions.concat(extensionMap.get(identifier));
}
return extensions;
}
}
function addDependenciesToDescriptors() {
for (let module of getModules()) {
let moduleJSONPath = path.resolve(FRONTEND_PATH, module, 'module.json');
let content = fs.readFileSync(moduleJSONPath).toString();
let moduleObj = parseJSON(content);
let existingDependencies = moduleObj.dependencies || [];
let dependencies =
existingDependencies.concat(getModuleDependencies(module))
.filter((depModule) => !MODULES_TO_REMOVE.includes(depModule))
.filter((depModule) => !(REMOVE_DEPENDENCIES_BY_EXISTING_MODULES[module] || []).includes(depModule));
let newDependenciesForExistingModule = NEW_DEPENDENCIES_BY_EXISTING_MODULES[module];
if (newDependenciesForExistingModule)
dependencies = dependencies.concat(newDependenciesForExistingModule);
if (dependencies.length)
moduleObj.dependencies = dependencies;
let newStringified = stringifyJSON(moduleObj);
if (stringifyJSON(moduleObj) !== stringifyJSON(parseJSON(content)))
fs.writeFileSync(moduleJSONPath, newStringified);
}
function getModuleDependencies(existingModule) {
let newDeps = [];
for (let newModule in DEPENDENTS_BY_MODULE) {
let dependents = DEPENDENTS_BY_MODULE[newModule];
if (dependents.includes(existingModule))
newDeps.push(newModule);
}
return newDeps;
}
}
function updateApplicationDescriptor(descriptorFileName, newModuleSet) {
let descriptorPath = path.join(FRONTEND_PATH, descriptorFileName);
let newModules = [...newModuleSet].filter(m => APPLICATIONS_BY_MODULE[m].includes(descriptorFileName));
if (newModules.length === 0)
return;
let includeNewModules = (acc, line) => {
if (line.includes('{') && line.endsWith('}')) {
line += ',';
acc.push(line);
return acc.concat(newModules.map((m, i) => {
// Need spacing to preserve indentation
let string;
if (MODULE_MAPPING[m].autostart)
string = ` { "name": "${m}", "type": "autostart" }`;
else
string = ` { "name": "${m}" }`;
if (i !== newModules.length - 1)
string += ',';
return string;
}));
}
return acc.concat([line]);
};
let removeModules = (acc, line) => MODULES_TO_REMOVE.every(m => !line.includes(m)) ? acc.concat([line]) : acc;
let lines =
fs.readFileSync(descriptorPath).toString().split('\n').reduce(includeNewModules, []).reduce(removeModules, []);
fs.writeFileSync(descriptorPath, lines.join('\n'));
}
function getModules() {
return fs.readdirSync(FRONTEND_PATH).filter(function(file) {
return fs.statSync(path.join(FRONTEND_PATH, file)).isDirectory() &&
utils.isFile(path.join(FRONTEND_PATH, file, 'module.json'));
});
}
function parseJSON(string) {
return JSON.parse(string);
}
function stringifyJSON(obj) {
return unicodeEscape(JSON.stringify(obj, null, 4) + '\n');
}
// http://stackoverflow.com/questions/7499473/need-to-escape-non-ascii-characters-in-javascript
function unicodeEscape(string) {
function padWithLeadingZeros(string) {
return new Array(5 - string.length).join('0') + string;
}
function unicodeCharEscape(charCode) {
return '\\u' + padWithLeadingZeros(charCode.toString(16));
}
return string.split('')
.map(function(char) {
var charCode = char.charCodeAt(0);
return charCode > 127 ? unicodeCharEscape(charCode) : char;
})
.join('');
}
if (require.main === module)
extractModule();