import { denoifySingleFileFactory } from "./denoifySingleFile"; import { transformCodebase } from "../tools/transformCodebase"; import { resolveNodeModuleToDenoModuleFactory } from "./resolveNodeModuleToDenoModule"; import * as fs from "fs"; import * as path from "path"; import * as commentJson from "comment-json"; import { denoifyImportExportStatementFactory } from "./denoifyImportExportStatement"; import { isInsideOrIsDir } from "../tools/isInsideOrIsDir"; import { getInstalledVersionPackageJsonFactory } from "./getInstalledVersionPackageJson"; import { toPosix } from "../tools/toPosix"; import { id } from "tsafe"; import { resolvePathsWithWildcards } from "../tools/resolvePathsWithWildcards"; import { arrPartition } from "evt/tools/reducers/partition"; import { fsCopy } from "../tools/fsCopy"; import { getDenoifyParamsWithCosmiconfig } from "./config/parseParams"; export async function denoify(params: { projectPath: string; srcDirPath: string | undefined; denoDistPath: string | undefined; indexFilePath: string | undefined; }) { process.chdir(params.projectPath ?? "."); const srcDirPath = params.srcDirPath !== undefined ? params.srcDirPath : ["src", "lib"].find(sourceDirPath => fs.existsSync(sourceDirPath)); console.log(`Denoify is reading sources files from ${srcDirPath}`); if (!srcDirPath) { throw new Error("No src directory found"); } const packageJsonParsed = JSON.parse(fs.readFileSync("package.json").toString("utf8")); const denoifyParams = await getDenoifyParamsWithCosmiconfig(); const tsconfigOutDir = getTsConfigOutDir(); const denoDistPath = (() => { if (params.denoDistPath !== undefined) { return params.denoDistPath; } if (denoifyParams?.out !== undefined) { return denoifyParams?.out; } if (tsconfigOutDir !== undefined) { return path.join(path.dirname(tsconfigOutDir), `deno_${path.basename(tsconfigOutDir)}`); } throw new Error(`You should specify output directory by --out option or specify "outDir" in tsconfig.json`); })(); console.log(`Deno distribution will be generated at ${denoDistPath}`); const { denoifySingleFile } = denoifySingleFileFactory( (() => { const { getInstalledVersionPackageJson } = getInstalledVersionPackageJsonFactory({ "projectPath": "." }); const { denoifyImportExportStatement } = denoifyImportExportStatementFactory( (() => { const { resolveNodeModuleToDenoModule } = resolveNodeModuleToDenoModuleFactory({ "userProvidedPorts": denoifyParams?.ports ?? {}, "dependencies": packageJsonParsed["dependencies"] ?? {}, "devDependencies": packageJsonParsed["devDependencies"] ?? {}, "log": console.log, getInstalledVersionPackageJson }); return id[0]>({ "getDestDirPath": ({ dirPath }) => path.join(denoDistPath, path.relative(srcDirPath, dirPath)), resolveNodeModuleToDenoModule, "userProvidedReplacerPath": denoifyParams?.replacer, getInstalledVersionPackageJson }); })() ); return { denoifyImportExportStatement }; })() ); await transformCodebase({ srcDirPath, "destDirPath": denoDistPath, "transformSourceCodeString": async ({ sourceCode, filePath }) => { if (/\.deno\.tsx?$/i.test(filePath)) { const nodeFilePath = filePath.replace(/\.deno\.ts/i, ".ts"); if (fs.existsSync(nodeFilePath)) { return undefined; } return { "modifiedSourceCode": sourceCode, "newFileName": path.basename(nodeFilePath) }; } if (!/\.(?:ts|tsx|js|jsx)$/i.test(filePath)) { return { "modifiedSourceCode": sourceCode }; } const denoVersionFilePath = (() => { const split = filePath.split("."); split.splice(split.length - 1, 0, "deno"); return split.join("."); })(); if (fs.existsSync(denoVersionFilePath)) { return { "modifiedSourceCode": fs.readFileSync(denoVersionFilePath).toString("utf8") }; } if (/^\s*\/\/\s*@denoify-ignore/.test(sourceCode)) { return undefined; } return { "modifiedSourceCode": await denoifySingleFile({ sourceCode, "dirPath": path.dirname(filePath) }) }; } }); generateModFile({ packageJsonParsed, tsconfigOutDir, denoDistPath, srcDirPath, "explicitlyProvidedIndexFilePath": params.indexFilePath ?? denoifyParams?.index }); { const includes = (denoifyParams?.includes ?? ["README.md", "LICENSE"]).map(pathOrObj => typeof pathOrObj === "string" ? path.normalize(pathOrObj) : { ...pathOrObj, "src": path.normalize(pathOrObj.src) } ); const [strIncludes, objIncludes] = arrPartition(includes, (include): include is string => typeof include === "string"); resolvePathsWithWildcards({ "pathWithWildcards": strIncludes }).forEach(resolvedPath => fsCopy(resolvedPath, path.join(denoDistPath, resolvedPath)) ); objIncludes.forEach(({ src, destDir, destBasename }) => fsCopy(src, path.join(denoDistPath, path.join(destDir ?? path.dirname(src), destBasename ?? path.basename(src)))) ); } } function getTsConfigOutDir(): string | undefined { const parsedTsCompile = commentJson.parse(fs.readFileSync("tsconfig.json").toString("utf8")); const { outDir } = parsedTsCompile["compilerOptions"]; if (!outDir) { return; } return path.normalize(outDir); } function generateModFile(params: { packageJsonParsed: any; tsconfigOutDir: string | undefined; denoDistPath: string; explicitlyProvidedIndexFilePath: string | undefined; srcDirPath: string; }) { const { denoDistPath, packageJsonParsed, tsconfigOutDir, explicitlyProvidedIndexFilePath, srcDirPath } = params; const indexFileRelativePath = getIndexFileRelativePath({ packageJsonParsed, tsconfigOutDir, explicitlyProvidedIndexFilePath, srcDirPath }); //No need to generate a mod.ts files, user have explicitly provided one. if (fs.existsSync(path.join(srcDirPath, "mod.ts"))) { return; } if (indexFileRelativePath === undefined) { console.warn( [ `Denoify did not generate "mod.ts" file because your index wasn't found. You have two options:`, `1) You may create ${path.join(srcDirPath, "mod.ts")} it will be denoified and moved to ${denoDistPath}.`, `2) You can also specify where is your index using the denoify.index field in package.json` ].join("\n") ); return; } if (fs.existsSync(path.resolve(denoDistPath, indexFileRelativePath))) { fs.writeFileSync( path.join(denoDistPath, "mod.ts"), Buffer.from(`export * from "${indexFileRelativePath.replace(/^(:?\.\/)?/, "./")}";`, "utf8") ); } } /** Relative to the src/ dir */ function getIndexFileRelativePath(params: { packageJsonParsed: any; tsconfigOutDir: string | undefined; explicitlyProvidedIndexFilePath: string | undefined; srcDirPath: string; }): string | undefined { const { packageJsonParsed, tsconfigOutDir, explicitlyProvidedIndexFilePath, srcDirPath } = params; if (explicitlyProvidedIndexFilePath !== undefined) { return path.relative(srcDirPath, toPosix(explicitlyProvidedIndexFilePath)); } if (tsconfigOutDir === undefined) { return; } if (!("main" in packageJsonParsed)) { return; } if ( !isInsideOrIsDir({ "dirPath": tsconfigOutDir, "fileOrDirPath": path.normalize(packageJsonParsed["main"]) }) ) { return; } const indexFileRelativePath = toPosix( path .relative( tsconfigOutDir, path.normalize(packageJsonParsed["main"]) // ./dist/lib/index.js ) // ./lib/index.js, NOTE: The the extension is commonly .js but it can be .cjs, .jsx... .replace(/\.[^.]+$/i, ".ts") // ./lib/index.ts ); return indexFileRelativePath; }