storybook/scripts/dts-localize.ts
2022-07-23 17:15:13 +02:00

299 lines
9.3 KiB
TypeScript

/* eslint-disable no-param-reassign */
import path, { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
import fs from 'fs-extra';
import { sync } from 'read-pkg-up';
import slash from 'slash';
import ts from 'typescript';
const parseConfigHost = {
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
readDirectory: ts.sys.readDirectory,
fileExists: ts.sys.fileExists,
readFile: ts.sys.readFile,
};
function getAbsolutePath(fileName: string, cwd?: string) {
if (!isAbsolute(fileName)) {
fileName = join(cwd !== undefined ? cwd : process.cwd(), fileName);
}
return fileName;
}
function findConfig(inputFiles: string[]) {
if (inputFiles.length !== 1) {
throw new Error(
'Cannot find tsconfig for multiple files. Please specify preferred tsconfig file'
);
}
// input file could be a relative path to the current path
// and desired config could be outside of current cwd folder
// so we have to provide absolute path to find config until the root
const searchPath = getAbsolutePath(inputFiles[0]);
const configFileName = ts.findConfigFile(searchPath, ts.sys.fileExists);
if (!configFileName) {
throw new Error(`Cannot find config file for file ${searchPath}`);
}
return configFileName;
}
function getCompilerOptions(inputFileNames: string[], preferredConfigPath?: string) {
const configFileName =
preferredConfigPath !== undefined ? preferredConfigPath : findConfig(inputFileNames);
const configParseResult = ts.readConfigFile(configFileName, ts.sys.readFile);
const compilerOptionsParseResult = ts.parseJsonConfigFileContent(
configParseResult.config,
parseConfigHost,
resolve(dirname(configFileName)),
undefined,
getAbsolutePath(configFileName)
);
return compilerOptionsParseResult.options;
}
interface Options {
externals: string[];
cwd?: string;
}
export const run = async (entrySourceFiles: string[], outputPath: string, options: Options) => {
const compilerOptions = getCompilerOptions(entrySourceFiles);
const host = ts.createCompilerHost(compilerOptions);
const cwd = options.cwd || process.cwd();
const pkg = sync({ cwd }).packageJson;
const externals = Object.keys({
...pkg.dependencies,
...pkg.peerDependencies,
});
// this to make paths for local packages as they are in node_modules because of yarn
// but it depends on the way you handle "flatting of files"
// so basically you can remove this host completely if you handle it in different way
host.realpath = (p: string) => p;
const program = ts.createProgram(entrySourceFiles, compilerOptions, host);
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
removeComments: false,
});
const typeChecker = program.getTypeChecker();
const sourceFiles = program.getSourceFiles();
const filesRemapping = new Map<string, string>();
const replaceRemapping = new Map<string, string[]>();
/**
* @param {string} basePath the path is the directory where the package.json is located
* @param {string} filePath the path of the current file
*/
function getReplacementPathRelativeToBase(basePath: string, filePath: string) {
const relativePath = relative(basePath, filePath);
let newPath = '';
/*
first we work out the relative path from the basePath
we might get a path like: ../../node_modules/packagename/dist/dir/file.ts
Here's a few examples of what the idea is:
../../node_modules/packagename/dist/dir/file.ts => _modules/packagename-dist-dir-file.ts
../../node_modules/packagename/node_modules/b/dist/dir/file.ts => _modules/packagename-node_modules-b-dist-dir-file.ts
./node_modules/packagename/dist/dir/file.ts => _modules/packagename-dist-dir-file.ts
./dist/ts-tmp/file.ts => file.ts
*/
if (relativePath.includes(`node_modules${sep}`)) {
const [, ...parts] = relativePath.split(`node_modules${sep}`);
const filename = parts.join(`node_modules${sep}`).split(sep).join('-');
newPath = join(outputPath, '_modules', filename);
} else if (relativePath.includes(join('dist', `ts-tmp${sep}`))) {
const [, ...parts] = relativePath.split(join('dist', `ts-tmp${sep}`));
const filename = parts.join('').split(sep).join('-');
newPath = join(outputPath, filename);
} else {
const filename = relativePath.split(sep).join('-');
newPath = join(outputPath, filename);
}
return newPath;
}
function wasReplacedAlready(fileName: string, target: string) {
// skipping this import because is has been previously replaced already
if (replaceRemapping.has(fileName) && replaceRemapping.get(fileName).includes(target)) {
return true;
}
return false;
}
function getReplacementPathRelativeToFile(
currentSourceFile: string,
referencedSourceFile: string
) {
filesRemapping.set(currentSourceFile, getReplacementPathRelativeToBase(cwd, currentSourceFile));
filesRemapping.set(
referencedSourceFile,
getReplacementPathRelativeToBase(cwd, referencedSourceFile)
);
const result = path
.relative(filesRemapping.get(currentSourceFile), filesRemapping.get(referencedSourceFile))
.slice(1)
.replace('.d.ts', '')
.replace('.ts', '');
replaceRemapping.set(currentSourceFile, [
...(replaceRemapping.get(currentSourceFile) || []),
result,
]);
return result;
}
function wasIgnored(target: string) {
if (externals.includes(target)) {
return true;
}
return false;
}
function getSourceFile(moduleNode: ts.Node) {
while (!ts.isSourceFile(moduleNode)) {
moduleNode = moduleNode.getSourceFile();
}
return moduleNode;
}
function replaceImport(node: ts.Node) {
if (
(ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
node.moduleSpecifier !== undefined
) {
// @ts-ignore
const target: string = node.moduleSpecifier.text;
let currentSourceFile = '';
let referencedSourceFile = '';
if (wasIgnored(target)) {
return true;
}
currentSourceFile = node.getSourceFile().fileName;
if (wasReplacedAlready(currentSourceFile, target)) {
return true;
}
// find the sourceFile the import is pointing to
referencedSourceFile = getSourceFile(
typeChecker.getSymbolAtLocation(node.moduleSpecifier).valueDeclaration
).fileName;
const replacementPath = getReplacementPathRelativeToFile(
currentSourceFile,
referencedSourceFile
);
// @ts-ignore
node.moduleSpecifier = ts.factory.createStringLiteral(replacementPath);
return true;
}
if (ts.isImportTypeNode(node)) {
const target = node.argument.getText().slice(1, -1);
let currentSourceFile = '';
let referencedSourceFile = '';
// check if the import's path is in the ignore-list
if (wasIgnored(target)) {
return true;
}
currentSourceFile = node.getSourceFile().fileName;
// check if it's already been replaced previously
if (wasReplacedAlready(currentSourceFile, target)) {
return true;
}
// find the sourceFile the import is pointing to
referencedSourceFile = getSourceFile(
typeChecker.getSymbolAtLocation(node).valueDeclaration
).fileName;
const replacementPath = getReplacementPathRelativeToFile(
currentSourceFile,
referencedSourceFile
);
// @ts-ignore
node.argument = ts.factory.createStringLiteral(replacementPath);
return true;
}
return undefined;
}
function walkNodeToReplaceImports(node: ts.Node) {
// it seems that it is unnecessary, but we're sure that it is impossible to have import statement later than we can just skip this node
if (replaceImport(node)) {
return;
}
ts.forEachChild(node, (n) => walkNodeToReplaceImports(n));
}
function outputSourceToFile(sourceFile: ts.SourceFile) {
const newPath = filesRemapping.get(sourceFile.fileName);
fs.outputFileSync(newPath, printer.printFile(sourceFile).trim());
}
function actOnSourceFile(sourceFile: ts.SourceFile & { resolvedModules?: Map<any, any> }) {
// console.log(sourceFile);
filesRemapping.set(
sourceFile.fileName,
getReplacementPathRelativeToBase(cwd, sourceFile.fileName)
);
walkNodeToReplaceImports(sourceFile);
outputSourceToFile(sourceFile);
// using a internal 'resolvedModules' API to get all the modules that were imported by this source file
// this seems to be a cache TypeScript uses internally
// I've been looking for a a public API to use, but so far haven't found it.
// I could create the dependency graph myself, perhaps that'd be better, but I'm OK with this for now.
if (sourceFile.resolvedModules) {
sourceFile.resolvedModules.forEach((v, k) => {
if (externals.includes(k)) {
return;
}
if (!v) {
return;
}
const x = sourceFiles.find((f) => f.fileName === v.resolvedFileName);
if (!x) {
return;
}
if (replaceRemapping.has(v.resolvedFileName)) {
return;
}
actOnSourceFile(sourceFiles.find((f) => f.fileName === v.resolvedFileName));
});
}
}
entrySourceFiles.forEach((file) => {
const sourceFile = sourceFiles.find((f) => f.fileName === slash(file));
actOnSourceFile(sourceFile);
});
};