mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-16 05:03:11 +08:00
299 lines
9.3 KiB
TypeScript
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);
|
|
});
|
|
};
|