365 строки
10 KiB
JavaScript
365 строки
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();
|