vscode/migrate.mjs

365 lines
10 KiB
JavaScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
//@ts-check
// *****************************************************************
// * *
// * AMD-TO-ESM MIGRATION SCRIPT *
// * *
// *****************************************************************
import { readFileSync, writeFileSync, unlinkSync } from 'node:fs';
import { join, extname, dirname, relative } from 'node:path';
import { preProcessFile } from 'typescript';
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
import { fileURLToPath } from 'node:url';
// @ts-expect-error
import watch from './build/lib/watch/index.js';
const enableWatching = !process.argv.includes('--disable-watch');
const enableInPlace = process.argv.includes('--enable-in-place');
const esmToAmd = process.argv.includes('--enable-esm-to-amd');
const amdToEsm = !esmToAmd;
const srcFolder = fileURLToPath(new URL('src', import.meta.url));
const dstFolder = fileURLToPath(new URL(enableInPlace ? 'src' : 'src2', import.meta.url));
const binaryFileExtensions = new Set([
'.svg', '.ttf', '.png', '.sh', '.html', '.json', '.zsh', '.scpt', '.mp3', '.fish', '.ps1', '.psm1', '.md', '.txt', '.zip', '.pdf', '.qwoff', '.jxs', '.tst', '.wuff', '.less', '.utf16le', '.snap', '.actual', '.tsx', '.scm'
]);
function migrate() {
console.log(`~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`);
console.log(`STARTING ${amdToEsm ? 'AMD->ESM' : 'ESM->AMD'} MIGRATION of ${enableInPlace ? 'src in-place' : 'src to src2'}.`);
// installing watcher quickly to avoid missing early events
const watchSrc = enableWatching ? watch('src/**', { base: 'src', readDelay: 200 }) : undefined;
/** @type {string[]} */
const files = [];
readdir(srcFolder, files);
for (const filePath of files) {
const fileContents = readFileSync(filePath);
migrateOne(filePath, fileContents);
}
if (amdToEsm) {
writeFileSync(join(dstFolder, 'package.json'), `{"type": "module"}`);
} else {
unlinkSync(join(dstFolder, 'package.json'));
}
if (!enableInPlace) {
writeFileSync(join(dstFolder, '.gitignore'), `*`);
}
console.log(`~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~`);
console.log(`COMPLETED ${amdToEsm ? 'AMD->ESM' : 'ESM->AMD'} MIGRATION of ${enableInPlace ? 'src in-place' : 'src to src2'}. You can now launch npm run watch-amd or npm run watch-client-amd`);
if (esmToAmd) {
console.log(`Make sure to set the environment variable VSCODE_BUILD_AMD to a string of value 'true' if you want to build VS Code as AMD`);
}
if (watchSrc) {
console.log(`WATCHING src for changes...`);
watchSrc.on('data', (e) => {
migrateOne(e.path, e.contents);
console.log(`Handled change event for ${e.path}.`);
});
}
}
/**
* @param filePath
* @param fileContents
*/
function migrateOne(filePath, fileContents) {
const fileExtension = extname(filePath);
if (fileExtension === '.ts') {
migrateTS(filePath, fileContents.toString());
} else if (filePath.endsWith('tsconfig.base.json')) {
const opts = JSON.parse(fileContents.toString());
if (amdToEsm) {
opts.compilerOptions.module = 'es2022';
opts.compilerOptions.allowSyntheticDefaultImports = true;
} else {
opts.compilerOptions.module = 'amd';
delete opts.compilerOptions.allowSyntheticDefaultImports;
}
writeDestFile(filePath, JSON.stringify(opts, null, '\t'));
} else if (fileExtension === '.js' || fileExtension === '.cjs' || fileExtension === '.mjs' || fileExtension === '.css' || binaryFileExtensions.has(fileExtension)) {
writeDestFile(filePath, fileContents);
} else {
console.log(`ignoring ${filePath}`);
}
}
/**
* @param fileContents
* @typedef {{pos:number;end:number;}} Import
* @return
*/
function discoverImports(fileContents) {
const info = preProcessFile(fileContents);
const search = /export .* from ['"]([^'"]+)['"]/g;
/** typedef {Import[]} */
let result = [];
do {
const m = search.exec(fileContents);
if (!m) {
break;
}
const end = m.index + m[0].length - 2;
const pos = end - m[1].length;
result.push({ pos, end });
} while (true);
result = result.concat(info.importedFiles);
result.sort((a, b) => {
return a.pos - b.pos;
});
for (let i = 1; i < result.length; i++) {
const prev = result[i - 1];
const curr = result[i];
if (prev.pos === curr.pos) {
result.splice(i, 1);
i--;
}
}
return result;
}
/**
* @param filePath
* @param fileContents
*/
function migrateTS(filePath, fileContents) {
if (filePath.endsWith('.d.ts')) {
return writeDestFile(filePath, fileContents);
}
const imports = discoverImports(fileContents);
/** @type {Replacement[]} */
const replacements = [];
for (let i = imports.length - 1; i >= 0; i--) {
const pos = imports[i].pos + 1;
const end = imports[i].end + 1;
const importedFilename = fileContents.substring(pos, end);
/** @type {string|undefined} */
let importedFilepath = undefined;
if (amdToEsm) {
if (/^vs\/css!/.test(importedFilename)) {
importedFilepath = importedFilename.substr('vs/css!'.length) + '.css';
} else {
importedFilepath = importedFilename;
}
} else {
if (importedFilename.endsWith('.css')) {
importedFilepath = `vs/css!${importedFilename.substr(0, importedFilename.length - 4)}`;
} else if (importedFilename.endsWith('.js')) {
importedFilepath = importedFilename.substr(0, importedFilename.length - 3);
}
}
if (typeof importedFilepath !== 'string') {
continue;
}
/** @type {boolean} */
let isRelativeImport;
if (amdToEsm) {
if (/(^\.\/)|(^\.\.\/)/.test(importedFilepath)) {
importedFilepath = join(dirname(filePath), importedFilepath);
isRelativeImport = true;
} else if (/^vs\//.test(importedFilepath)) {
importedFilepath = join(srcFolder, importedFilepath);
isRelativeImport = true;
} else {
importedFilepath = importedFilepath;
isRelativeImport = false;
}
} else {
importedFilepath = importedFilepath;
isRelativeImport = false;
}
/** @type {string} */
let replacementImport;
if (isRelativeImport) {
replacementImport = generateRelativeImport(filePath, importedFilepath);
} else {
replacementImport = importedFilepath;
}
replacements.push({ pos, end, text: replacementImport });
}
fileContents = applyReplacements(fileContents, replacements);
writeDestFile(filePath, fileContents);
}
/**
* @param filePath
* @param importedFilepath
*/
function generateRelativeImport(filePath, importedFilepath) {
/** @type {string} */
let relativePath;
// See https://github.com/microsoft/TypeScript/issues/16577#issuecomment-754941937
if (!importedFilepath.endsWith('.css') && !importedFilepath.endsWith('.cjs')) {
importedFilepath = `${importedFilepath}.js`;
}
relativePath = relative(dirname(filePath), `${importedFilepath}`);
relativePath = relativePath.replace(/\\/g, '/');
if (!/(^\.\/)|(^\.\.\/)/.test(relativePath)) {
relativePath = './' + relativePath;
}
return relativePath;
}
/** @typedef {{pos:number;end:number;text:string;}} Replacement */
/**
* @param str
* @param replacements
*/
function applyReplacements(str, replacements) {
replacements.sort((a, b) => {
return a.pos - b.pos;
});
/** @type {string[]} */
const result = [];
let lastEnd = 0;
for (const replacement of replacements) {
const { pos, end, text } = replacement;
result.push(str.substring(lastEnd, pos));
result.push(text);
lastEnd = end;
}
result.push(str.substring(lastEnd, str.length));
return result.join('');
}
/**
* @param srcFilePath
* @param fileContents
*/
function writeDestFile(srcFilePath, fileContents) {
const destFilePath = srcFilePath.replace(srcFolder, dstFolder);
ensureDir(dirname(destFilePath));
if (/(\.ts$)|(\.js$)|(\.html$)/.test(destFilePath)) {
fileContents = toggleComments(fileContents);
}
/** @type {Buffer | undefined} */
let existingFileContents = undefined;
try {
existingFileContents = readFileSync(destFilePath);
} catch (err) { }
if (!buffersAreEqual(existingFileContents, fileContents)) {
writeFileSync(destFilePath, fileContents);
}
/**
* @param fileContents
*/
function toggleComments(fileContents) {
const lines = String(fileContents).split(/\r\n|\r|\n/);
let mode = 0;
let didChange = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (mode === 0) {
if (amdToEsm ? /\/\/ ESM-comment-begin/.test(line) : /\/\/ ESM-uncomment-begin/.test(line)) {
mode = 1;
continue;
}
if (amdToEsm ? /\/\/ ESM-uncomment-begin/.test(line) : /\/\/ ESM-comment-begin/.test(line)) {
mode = 2;
continue;
}
continue;
}
if (mode === 1) {
if (amdToEsm ? /\/\/ ESM-comment-end/.test(line) : /\/\/ ESM-uncomment-end/.test(line)) {
mode = 0;
continue;
}
didChange = true;
lines[i] = line.replace(/^\s*/, (match) => match + '// ');
continue;
}
if (mode === 2) {
if (amdToEsm ? /\/\/ ESM-uncomment-end/.test(line) : /\/\/ ESM-comment-end/.test(line)) {
mode = 0;
continue;
}
didChange = true;
lines[i] = line.replace(/^(\s*)\/\/ ?/, function (_, indent) {
return indent;
});
}
}
if (didChange) {
return lines.join('\n');
}
return fileContents;
}
}
/**
* @param existingFileContents
* @param fileContents
*/
function buffersAreEqual(existingFileContents, fileContents) {
if (!existingFileContents) {
return false;
}
if (typeof fileContents === 'string') {
fileContents = Buffer.from(fileContents);
}
return existingFileContents.equals(fileContents);
}
const ensureDirCache = new Set();
function ensureDir(dirPath) {
if (ensureDirCache.has(dirPath)) {
return;
}
ensureDirCache.add(dirPath);
ensureDir(dirname(dirPath));
if (!existsSync(dirPath)) {
mkdirSync(dirPath);
}
}
function readdir(dirPath, result) {
const entries = readdirSync(dirPath);
for (const entry of entries) {
const entryPath = join(dirPath, entry);
const stat = statSync(entryPath);
if (stat.isDirectory()) {
readdir(join(dirPath, entry), result);
} else {
result.push(entryPath);
}
}
}
migrate();