tsconfig-to-dual-package
Version:
A simple tool that add package.json({"type":"commonjs"/"module"}) to TypeScript outDir for dual package.
152 lines (151 loc) • 6.23 kB
JavaScript
import fs from "node:fs/promises";
import path from "node:path";
import { resolveTsConfig } from "resolve-tsconfig";
import ts from "typescript";
const formatDiagnostics = (diagnostics) => {
return diagnostics
.map((diagnostic) => {
if (diagnostic.file) {
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
return `${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`;
}
else {
return ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
}
})
.join("\n");
};
export const findNearestPackageJson = async ({ cwd }) => {
// cwd is root, throw and error
if (cwd === path.dirname(cwd)) {
throw new Error("Failed to find package.json");
}
const packageJsonFilePath = path.join(cwd, "package.json");
const exists = await fs.stat(packageJsonFilePath).catch(() => false);
if (!exists) {
return findNearestPackageJson({
cwd: path.dirname(cwd)
});
}
return JSON.parse(await fs.readFile(packageJsonFilePath, "utf-8"));
};
export const findTsConfig = async ({ targetTsConfigFilePaths, cwd }) => {
if (targetTsConfigFilePaths && targetTsConfigFilePaths.length > 0) {
return targetTsConfigFilePaths.map((value) => {
return path.resolve(cwd, value);
});
}
const dirents = await fs.readdir(cwd, { withFileTypes: true });
// ok: tsconfig.json
// ok: tsconfig.cjs.json
// ok: tsconfig-cjs.json
// ng: cjs-tsconfig.json
// ng: tsconfig.json.cjs
return dirents
.filter((dirent) => {
// remove non-tsconfig.json
return path.extname(dirent.name) === ".json" && dirent.name.startsWith("tsconfig");
})
.map((dirent) => {
return path.join(cwd, dirent.name);
});
};
export const createModuleTypePackage = async ({ cwd, type, debug }) => {
// if the field has relative path, it should not be included in generated package.json
// because it is different relative path from the package.json
// https://docs.npmjs.com/cli/v9/configuring-npm/package-json
// https://nodejs.org/api/packages.html#community-conditions-definitions
const OMIT_FIELDS = ["main", "module", "browser", "types", "exports"];
try {
const basePkg = JSON.parse(await fs.readFile(path.resolve(cwd, "package.json"), "utf-8"));
const filteredPkg = Object.fromEntries(Object.entries(basePkg).filter(([key]) => !OMIT_FIELDS.includes(key)));
return {
...filteredPkg,
type
};
}
catch (e) {
if (debug) {
console.error("Failed to load package.json", {
cwd,
type,
error: e
});
}
throw new Error(`Failed to read package.json in ${cwd}`, {
cause: e
});
}
};
const getModuleType = ({ packageType, moduleKind }) => {
// TODO: get more better way
if (moduleKind >= ts.ModuleKind.ES2015 && moduleKind <= ts.ModuleKind.ESNext) {
return "module";
}
else if (moduleKind >= ts.ModuleKind.Node16 && moduleKind <= ts.ModuleKind.NodeNext) {
// use package.json's type
// if type is not set, use commonjs
// > The emitted JavaScript uses either CommonJS or ES2020 output depending on the file extension and the value of the type setting in the nearest package.json. Module resolution also works differently.
// https://www.typescriptlang.org/tsconfig#node16nodenext-nightly-builds
// https://www.typescriptlang.org/docs/handbook/esm-node.html
return packageType !== null && packageType !== void 0 ? packageType : "commonjs";
}
else if (moduleKind === ts.ModuleKind.CommonJS) {
return "commonjs";
}
throw new Error("Non-support module kind: " + moduleKind);
};
export const tsconfigToDualPackages = async ({ targetTsConfigFilePaths, cwd, debug }) => {
const debugWithDefault = debug !== null && debug !== void 0 ? debug : false;
const packageJson = await findNearestPackageJson({ cwd });
// search tsconfig*.json
const tsconfigFilePaths = await findTsConfig({ targetTsConfigFilePaths, cwd });
// load tsconfig.json
const tsconfigs = await Promise.all(tsconfigFilePaths.map(async (tsconfigFilePath) => {
return {
filePath: tsconfigFilePath,
tsconfig: await resolveTsConfig({ filePath: tsconfigFilePath })
};
}));
// create package.json for dual package
const dualPackages = await Promise.all(tsconfigs.map(async ({ filePath, tsconfig }) => {
if (tsconfig.diagnostics || !tsconfig.config) {
const error = new Error(`Failed to load tsconfig: ${filePath}
${formatDiagnostics(tsconfig.diagnostics)}
`);
if (debugWithDefault) {
console.error({
error,
filePath,
tsconfig
});
}
throw error;
}
if (!tsconfig.config.options.outDir) {
throw new Error(`Failed to find "outDir" option in tsconfig.json`);
}
if (!tsconfig.config.options.module) {
throw new Error(`Failed to find "module" option in tsconfig.json`);
}
return {
outDir: tsconfig.config.options.outDir,
pkg: await createModuleTypePackage({
cwd,
type: getModuleType({
packageType: packageJson.type,
moduleKind: tsconfig.config.options.module
}),
debug: debugWithDefault
})
};
}));
// write to <outDir>/package.json
await Promise.all(dualPackages.map(async (dualPackage) => {
const { outDir, pkg } = await dualPackage;
await fs.mkdir(path.resolve(cwd, outDir), { recursive: true });
await fs.writeFile(path.resolve(cwd, outDir, "package.json"), JSON.stringify(pkg, null, 2), "utf-8");
}));
};
//# sourceMappingURL=tsconfig-to-dual-package.js.map