#!/usr/bin/env node import * as path from "path"; import { pathDepth } from "../tools/pathDepth"; import { moveContentUpOneLevelFactory } from "../tools/moveContentUpOneLevel"; import { getIsDryRun } from "./lib/getIsDryRun"; import * as fs from "fs"; import * as commentJson from "comment-json"; import { resolvePathsWithWildcards } from "../tools/resolvePathsWithWildcards"; import { assert } from "tsafe/assert"; import { is } from "tsafe/is"; import { removeDuplicates } from "evt/tools/reducers/removeDuplicates"; import { same } from "evt/tools/inDepth/same"; import { isInsideOrIsDir } from "../tools/isInsideOrIsDir"; /** * To disable dry run mode DRY_RUN=1 env variable must be set. * This function Change change the working directory. * */ async function run(params: { pathToTargetModule: string; isDryRun: boolean }) { const { isDryRun } = params; process.chdir(params.pathToTargetModule); if (fs.existsSync(".npmignore")) { throw new Error(".npmignore not supported, please use package.json 'files' instead"); } const packageJsonRaw = fs.readFileSync("package.json").toString("utf8"); const packageJsonParsed = JSON.parse(packageJsonRaw); const packageJsonFilesResolved: string[] | undefined = (() => { const pathWithWildcards: string[] | undefined = packageJsonParsed.files; return !pathWithWildcards ? undefined : resolvePathsWithWildcards({ pathWithWildcards }); })(); const { srcDirPath, tsconfigOutDir } = { "srcDirPath": ["src", "lib"].find(sourceDirPath => fs.existsSync(sourceDirPath)), "tsconfigOutDir": commentJson.parse( fs.readFileSync(fs.existsSync("./tsproject.json") ? "./tsproject.json" : "./tsconfig.json").toString("utf8") )["compilerOptions"]["outDir"] as string }; if (srcDirPath === undefined) { throw new Error("There should be a 'src' or 'lib' dir containing the .ts files"); } if (pathDepth(tsconfigOutDir) !== 1) { throw new Error("For this script to work tsconfig outDir must be a directory at the root of the project"); } const moveSourceFiles = "types" in packageJsonParsed ? !packageJsonParsed["types"].match(/\.d\.ts$/i) : false; console.log(moveSourceFiles ? "Putting .ts files alongside .js files" : "Leaving .ts file in the src/ directory"); const { beforeMovedFilePaths, moveFiles } = await (async () => { const getBeforeMovedFilePaths = async (isDryRun: boolean, log: typeof console.log | undefined) => { const { moveContentUpOneLevel } = moveContentUpOneLevelFactory({ isDryRun, log }); const moveAndGetBeforePath = (dirPath: string) => moveContentUpOneLevel({ dirPath }).then(({ beforeMovedFilePaths }) => beforeMovedFilePaths.map(filePath => path.join(dirPath, filePath)) ); return [...(await moveAndGetBeforePath(tsconfigOutDir)), ...(moveSourceFiles ? await moveAndGetBeforePath(srcDirPath) : [])]; }; return { "beforeMovedFilePaths": await getBeforeMovedFilePaths(true, undefined), "moveFiles": async () => { await getBeforeMovedFilePaths(isDryRun, undefined); } }; })(); const getAfterMovedFilePath = (params: { beforeMovedFilePath: string }) => { const { beforeMovedFilePath } = params; const beforeMovedFilePathFromList = beforeMovedFilePaths.find( beforeMovedFilePathFromList => path.relative(beforeMovedFilePathFromList, beforeMovedFilePath) === "" ); if (beforeMovedFilePathFromList === undefined) { if (!isInsideOrIsDir({ "dirPath": tsconfigOutDir, "fileOrDirPath": beforeMovedFilePath })) { //NOTE: In case the path would be absolute. return "./" + path.relative(".", beforeMovedFilePath); } throw new Error(`${beforeMovedFilePath} is inside ${tsconfigOutDir} but it has't been moved`); } const afterMovedFilePath = "./" + beforeMovedFilePathFromList .replace(/^\.\//, "") .split(path.sep) .filter((...[, index]) => index !== 0) .join(path.sep); return afterMovedFilePath; }; update_package_json: { const newPackageJsonRaw = JSON.stringify( { ...packageJsonParsed, ...("main" in packageJsonParsed ? { "main": getAfterMovedFilePath({ "beforeMovedFilePath": packageJsonParsed["main"] }) } : {}), ...("types" in packageJsonParsed ? { "types": getAfterMovedFilePath({ "beforeMovedFilePath": packageJsonParsed["types"] }) } : {}), ...("module" in packageJsonParsed ? { "module": getAfterMovedFilePath({ "beforeMovedFilePath": packageJsonParsed["module"] }) } : {}), ...("exports" in packageJsonParsed ? { "exports": Object.fromEntries( Object.entries(packageJsonParsed["exports"]) .map(([exportPath, exportPathResolutionOptions]): [string, RecObj][] => { assert(is(exportPathResolutionOptions)); if (!exportPath.includes("*")) { let json = JSON.stringify(exportPathResolutionOptions); for (const path of collectStrings(exportPathResolutionOptions)) { json = replaceAllOccurrences({ "str": json, "searchValue": path, "replaceValue": getAfterMovedFilePath({ "beforeMovedFilePath": path }) }); } return [[exportPath, JSON.parse(json)]]; } else { let result: { wildcardMatch: string; exportPathResolutionOption: RecObj; }[]; try { result = resolveExportsResolutionOptionsWithWildcard({ "exportPathResolutionOptionsWithWildcards": exportPathResolutionOptions, "resolvePathWithExportsWildcard": ({ pathWithExportsWildcard }) => resolvePathsWithWildcards({ "pathWithWildcards": [ (() => { assert(pathWithExportsWildcard.split("*").length === 2); return pathWithExportsWildcard.replace("*", "**/*"); })() ] }), getAfterMovedFilePath }); } catch (error) { console.log(String(error)); return []; } return result.map(({ wildcardMatch, exportPathResolutionOption }) => [ exportPath.replace("*", wildcardMatch), exportPathResolutionOption ]); } }) .flat() ) } : {}), ...("bin" in packageJsonParsed ? { "bin": (() => { const out: Record = {}; Object.keys(packageJsonParsed.bin) .map(binName => [binName, packageJsonParsed.bin[binName]] as const) .forEach( async ([binName, beforeMovedBinFilePath]) => (out[binName] = getAfterMovedFilePath({ "beforeMovedFilePath": beforeMovedBinFilePath })) ); return out; })() } : {}), ...(!!packageJsonFilesResolved ? { "files": packageJsonFilesResolved .map(beforeMovedFilesFilePath => getAfterMovedFilePath({ "beforeMovedFilePath": beforeMovedFilesFilePath })) .map(filePath => filePath.replace(/^\.\//, "")) } : {}), "scripts": undefined }, null, (packageJsonRaw.replace(/\t/g, " ").match(/^(\s*)\"name\"/m) ?? [{ "length": 2 }])[1].length ) + packageJsonRaw.match(/}([\r\n]*)$/)![1]; console.log(`${isDryRun ? "(dry)" : ""} package.json:\n${newPackageJsonRaw}`); if (isDryRun) { break update_package_json; } fs.writeFileSync("package.json", Buffer.from(newPackageJsonRaw, "utf8")); } await moveFiles(); beforeMovedFilePaths .filter(beforeMovedFilePath => /\.[cm]?js\.map$/.test(beforeMovedFilePath)) .forEach(beforeMovedSourceMapFilePath => { const afterMovedSourceMapFilePath = getAfterMovedFilePath({ "beforeMovedFilePath": beforeMovedSourceMapFilePath }); const sourceMapParsed: { sources: string[] } = JSON.parse( fs.readFileSync(isDryRun ? beforeMovedSourceMapFilePath : afterMovedSourceMapFilePath).toString("utf8") ); const sources = sourceMapParsed.sources.map(filePath => path.relative( path.dirname(afterMovedSourceMapFilePath), getAfterMovedFilePath({ "beforeMovedFilePath": path.join(path.dirname(beforeMovedSourceMapFilePath), filePath) }) ) ); console.log( [ `${isDryRun ? "(dry) " : ""}Editing ${path.basename(beforeMovedSourceMapFilePath)}:`, JSON.stringify({ "sources": sourceMapParsed.sources }), `-> ${JSON.stringify({ sources })}` ].join(" ") ); walk: { if (isDryRun) { break walk; } fs.writeFileSync( afterMovedSourceMapFilePath, Buffer.from( JSON.stringify({ ...sourceMapParsed, sources }), "utf8" ) ); } }); } type RecObj = string | Record; function collectStrings(objRec: RecObj): string[] { if (typeof objRec === "string") { return [objRec]; } return Object.values(objRec) .map(value => { assert(is(value)); return collectStrings(value); }) .flat() .reduce(...removeDuplicates()); } const replaceAllOccurrences = (params: { str: string; searchValue: string; replaceValue: string }) => { const { str, searchValue, replaceValue } = params; return str.split(searchValue).join(replaceValue); }; function resolveExportsResolutionOptionsWithWildcard(params: { exportPathResolutionOptionsWithWildcards: RecObj; resolvePathWithExportsWildcard: (params: { pathWithExportsWildcard: string }) => string[]; getAfterMovedFilePath: (params: { beforeMovedFilePath: string }) => string; }): { wildcardMatch: string; exportPathResolutionOption: RecObj; }[] { /* { "require": "./dist/*.js", "import": "./dist/esm/*.mjs" } ----> collectStrings ---> ["./dist/*.js", "./dist/esm/*.mjs"] "./dist/*.js" --> resolvePathWithExportWildcard ---> [ "./dist/a.js", "./dist/b.js" ] "./dist/esm/*.js" --> resolvePathWithExportsWildcard ---> [ "./dist/esm/a.js", "./dist/esm/b.js" ] "./dist/*.js" , [ "./dist/a.js", "./dist/b.js" ] --> getWildcardMatches --> [ "a", "b" ] "./dist/esm/*.js" , [ "./dist/esm/a.js", "./dist/esm/b.js" ] --> getWildcardMatches --> [ "b", "a" ] assert(sameSet(["a", "b"], ["b", "a"])); loop over the wildcard matches, first a '{ "require": "./dist/*.js", "import": "./dist/esm/*.mjs" }', "*", "a" --> replaceAllOccurrences --> '{ "require": "./dist/a.js", "import": "./dist/esm/a.mjs" }' "./dist/a.js" --> getAfterMovedFilePath --> "./a.js" "./dist/b.js" --> getAfterMovedFilePath --> "./b.js" "./dist/esm/a.js" --> getAfterMovedFilePath --> "./esm/a.js" "./dist/esm/b.js" --> getAfterMovedFilePath --> "./esm/b.js" '{ "require": "./dist/a.js", "import": "./dist/esm/a.mjs" }', "./dist/a.js", "./a.js" --> replaceAllOccurrences --> '{ "require": "./a.js", "import": ".dist/esm/a.mjs" }' '{ "require": "./dist/a.js", "import": "./dist/esm/a.mjs" }', "./dist/b.js", "./b.js" --> replaceAllOccurrences --> '{ "require": "./a.js", "import": ".dist/esm/a.mjs" }' (unchanged) '{ "require": "./dist/a.js", "import": "./dist/esm/a.mjs" }', "./dist/esm/a.js", "./esm/a.js" --> replaceAllOccurrences --> '{ "require": "./a.js", "import": "./esm/a.mjs" }' '{ "require": "./dist/a.js", "import": "./dist/esm/a.mjs" }', "./dist/esm/b.js", "./esm/b.js" --> replaceAllOccurrences --> '{ "require": "./a.js", "import": "./esm/a.mjs" }' (unchanged) One entry of the return { "wildcardMatch": "a", "exportPathResolutionOption": JSON.parse('{ "require": "./a.js", "import": "./esm/a.mjs" }') } end loop */ const getWildcardMatches = (params: { pathWithExportWildcard: string; paths: string[] }): string[] => { const { pathWithExportWildcard, paths } = params; const [prefix, suffix] = pathWithExportWildcard.split("*"); return paths.map(path => path.replace(prefix, "").replace(suffix, "")); }; const { exportPathResolutionOptionsWithWildcards, resolvePathWithExportsWildcard, getAfterMovedFilePath } = params; const pathWithExportsWildcards = collectStrings(exportPathResolutionOptionsWithWildcards); const pathsByPathWithExportsWildcard: Record = Object.fromEntries( pathWithExportsWildcards.map(pathWithExportsWildcard => [ pathWithExportsWildcard, resolvePathWithExportsWildcard({ pathWithExportsWildcard }).map(path => `./${path}`) ]) ); let wildcardMatches: string[] | undefined = undefined; for (const pathWithExportWildcard of pathWithExportsWildcards) { const wildcardMatches_i = getWildcardMatches({ pathWithExportWildcard, "paths": pathsByPathWithExportsWildcard[pathWithExportWildcard] }); if (wildcardMatches === undefined) { wildcardMatches = wildcardMatches_i; continue; } assert(same(wildcardMatches, wildcardMatches_i, { "takeIntoAccountArraysOrdering": false })); } assert(wildcardMatches !== undefined); return wildcardMatches.map(wildcardMatch => { let json = JSON.stringify(exportPathResolutionOptionsWithWildcards); json = replaceAllOccurrences({ "str": json, "searchValue": "*", "replaceValue": wildcardMatch }); for (const path of Object.values(pathsByPathWithExportsWildcard).flat()) { json = replaceAllOccurrences({ "str": json, "searchValue": path, "replaceValue": getAfterMovedFilePath({ "beforeMovedFilePath": path }) }); } return { wildcardMatch, "exportPathResolutionOption": JSON.parse(json) }; }); } if (require.main === module) { process.once("unhandledRejection", error => { throw error; }); const { isDryRun } = getIsDryRun(); run({ "pathToTargetModule": ".", isDryRun }); }