UNPKG

@decaf-ts/utils

Version:

module management utils for decaf-ts

1,105 lines 55.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.BuildScripts = void 0; exports.parseList = parseList; exports.packageToGlobal = packageToGlobal; exports.getPackageDependencies = getPackageDependencies; const command_js_1 = require("./../command.cjs"); const constants_js_1 = require("./../constants.cjs"); const index_js_1 = require("./../../utils/index.cjs"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const plugin_typescript_1 = __importDefault(require("@rollup/plugin-typescript")); const plugin_commonjs_1 = __importDefault(require("@rollup/plugin-commonjs")); const plugin_node_resolve_1 = require("@rollup/plugin-node-resolve"); const plugin_json_1 = __importDefault(require("@rollup/plugin-json")); const child_process_1 = require("child_process"); const module_1 = require("module"); const logging_1 = require("@decaf-ts/logging"); const ts = __importStar(require("typescript")); const typescript_1 = require("typescript"); function parseList(input) { if (!input) return []; if (Array.isArray(input)) return input.map((i) => `${i}`.trim()).filter(Boolean); return `${input}` .split(",") .map((p) => p.trim()) .filter(Boolean); } function packageToGlobal(name) { // Remove scope and split by non-alphanumeric chars, then camelCase const withoutScope = name.replace(/^@/, ""); const parts = withoutScope.split(/[/\-_.]+/).filter(Boolean); return parts .map((p, i) => i === 0 ? p.replace(/[^a-zA-Z0-9]/g, "") : `${p.charAt(0).toUpperCase()}${p.slice(1)}`) .join(""); } function getPackageDependencies() { // Try the current working directory first let pkg; try { pkg = (0, index_js_1.getPackage)(process.cwd()); } catch { pkg = undefined; } // If no dependencies found in cwd, try the package next to this source file (fallback for tests) try { const hasDeps = pkg && (Object.keys(pkg.dependencies || {}).length > 0 || Object.keys(pkg.devDependencies || {}).length > 0 || Object.keys(pkg.peerDependencies || {}).length > 0); if (!hasDeps) { const fallbackDir = path_1.default.resolve(__dirname, "../../.."); try { pkg = (0, index_js_1.getPackage)(fallbackDir); } catch { // ignore and keep pkg as-is } } } catch { // ignore } const deps = Object.keys((pkg && pkg.dependencies) || {}); const peer = Object.keys((pkg && pkg.peerDependencies) || {}); const dev = Object.keys((pkg && pkg.devDependencies) || {}); return Array.from(new Set([...deps, ...peer, ...dev])); } // eslint-disable-next-line @typescript-eslint/no-unused-vars function buildExportsTypePathMappings(cwd = process.cwd(), deps = getPackageDependencies()) { const mappings = {}; for (const dep of deps) { const pkgPath = path_1.default.join(cwd, "node_modules", dep, "package.json"); if (!fs_1.default.existsSync(pkgPath)) continue; let pkg; try { pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, "utf8")); } catch { continue; } const exportsField = pkg?.exports; if (!exportsField || typeof exportsField !== "object") continue; for (const [subpath, target] of Object.entries(exportsField)) { if (!target || typeof target !== "object") continue; const typesPath = target.types; if (typeof typesPath !== "string" || !typesPath.length) continue; const normalizedSubpath = String(subpath).replace(/^\.\//, ""); const normalizedSpecifier = subpath === "." ? dep : `${dep}/${normalizedSubpath}`; const normalizedTypesPath = `./node_modules/${dep}/${typesPath.replace(/^\.\//, "")}`; mappings[normalizedSpecifier] = [normalizedTypesPath]; // Mirror wildcard export mappings so TypeScript can resolve deep import types. if (normalizedSubpath.endsWith("/*") && normalizedTypesPath.endsWith("/*")) { const specifierNoWildcard = normalizedSpecifier.slice(0, -2); const typesNoWildcard = normalizedTypesPath.slice(0, -2); if (specifierNoWildcard && typesNoWildcard) { mappings[specifierNoWildcard] = [typesNoWildcard]; } } } } return mappings; } const VERSION_STRING = "##VERSION##"; const COMMIT_STRING = "##COMMIT##"; const FULL_VERSION_STRING = "##FULL_VERSION##"; const PACKAGE_STRING = "##PACKAGE##"; const PACKAGE_SIZE_STRING = "##PACKAGE_SIZE##"; var Modes; (function (Modes) { Modes["CJS"] = "commonjs"; Modes["ESM"] = "es2022"; })(Modes || (Modes = {})); var BuildMode; (function (BuildMode) { BuildMode["BUILD"] = "build"; BuildMode["BUNDLE"] = "bundle"; BuildMode["ALL"] = "all"; })(BuildMode || (BuildMode = {})); var TsBuildTarget; (function (TsBuildTarget) { TsBuildTarget["ESM"] = "esm"; TsBuildTarget["CJS_CHECK"] = "cjs-check"; TsBuildTarget["TYPES"] = "types"; TsBuildTarget["NODE_NEXT_VALIDATE"] = "nodenext-validate"; TsBuildTarget["BUNDLE"] = "bundle"; })(TsBuildTarget || (TsBuildTarget = {})); const options = { prod: { type: "boolean", default: false, }, dev: { type: "boolean", default: false, }, buildMode: { type: "string", default: BuildMode.ALL, }, includes: { type: "string", default: "", }, externals: { type: "string", default: "", }, docs: { type: "boolean", default: false, }, commands: { type: "boolean", default: false, }, entry: { type: "string", default: "./src/index.ts", }, banner: { type: "boolean", default: false, }, validateNodeNext: { type: "boolean", default: false, }, }; const cjs2Transformer = (ext = ".cjs") => { const log = BuildScripts.log.for(cjs2Transformer); const resolutionCache = new Map(); return (transformationContext) => { return (sourceFile) => { const sourceDir = path_1.default.dirname(sourceFile.fileName); function resolvePath(importPath) { const cacheKey = JSON.stringify([sourceDir, importPath]); const cachedValue = resolutionCache.get(cacheKey); if (cachedValue != null) return cachedValue; let resolvedPath = importPath; try { resolvedPath = path_1.default.resolve(sourceDir, resolvedPath + ".ts"); } catch (error) { throw new Error(`Failed to resolve path ${importPath}: ${error}`); } let stat; try { stat = fs_1.default.statSync(resolvedPath); } catch (e) { try { log.verbose(`Testing existence of path ${resolvedPath} as a folder defaulting to index file`); stat = fs_1.default.statSync(resolvedPath.replace(/\.ts$/gm, "")); } catch (e2) { throw new Error(`Failed to resolve path ${importPath}: ${e}, ${e2}`); } } if (stat.isDirectory()) resolvedPath = resolvedPath.replace(/\.ts$/gm, "/index.ts"); if (path_1.default.isAbsolute(resolvedPath)) { const extension = (/\.tsx?$/.exec(path_1.default.basename(resolvedPath)) || [])[0] || void 0; resolvedPath = "./" + path_1.default.relative(sourceDir, path_1.default.resolve(path_1.default.dirname(resolvedPath), path_1.default.basename(resolvedPath, extension) + ext)); } resolutionCache.set(cacheKey, resolvedPath); return resolvedPath; } function visitNode(node) { if (shouldMutateModuleSpecifier(node)) { if (ts.isImportDeclaration(node)) { const resolvedPath = resolvePath(node.moduleSpecifier.text); const newModuleSpecifier = transformationContext.factory.createStringLiteral(resolvedPath); return transformationContext.factory.updateImportDeclaration(node, node.modifiers, node.importClause, newModuleSpecifier, undefined); } else if (ts.isExportDeclaration(node)) { const resolvedPath = resolvePath(node.moduleSpecifier.text); const newModuleSpecifier = transformationContext.factory.createStringLiteral(resolvedPath); return transformationContext.factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, newModuleSpecifier, undefined); } } else if (ts.isCallExpression(node)) { const moduleSpecifier = getCallExpressionModuleSpecifier(node); if (moduleSpecifier && isRelativePathWithoutExtension(moduleSpecifier.text)) { const resolvedPath = resolvePath(moduleSpecifier.text); const newModuleSpecifier = transformationContext.factory.createStringLiteral(resolvedPath); const updatedArguments = node.arguments.map((arg, index) => index === 0 ? newModuleSpecifier : arg); return transformationContext.factory.updateCallExpression(node, node.expression, node.typeArguments, transformationContext.factory.createNodeArray(updatedArguments)); } } return ts.visitEachChild(node, visitNode, transformationContext); } function shouldMutateModuleSpecifier(node) { if (!ts.isImportDeclaration(node) && !ts.isExportDeclaration(node)) return false; if (node.moduleSpecifier === undefined) return false; // only when module specifier is valid if (!ts.isStringLiteral(node.moduleSpecifier)) return false; return isRelativePathWithoutExtension(node.moduleSpecifier.text); } function isRelativePathWithoutExtension(rawPath) { if (!rawPath.startsWith("./") && !rawPath.startsWith("../")) return false; return !hasKnownFileExtension(rawPath); } function getCallExpressionModuleSpecifier(node) { if (isDynamicImportCall(node) && node.arguments.length > 0 && ts.isStringLiteral(node.arguments[0])) { return node.arguments[0]; } if (ts.isIdentifier(node.expression) && node.expression.text === "require" && node.arguments.length > 0 && ts.isStringLiteral(node.arguments[0])) { return node.arguments[0]; } return undefined; } function isDynamicImportCall(node) { return node.expression.kind === ts.SyntaxKind.ImportKeyword; } return ts.visitNode(sourceFile, visitNode); }; }; }; function withExtension(filePath, extension) { const parsed = path_1.default.parse(filePath); return path_1.default.join(parsed.dir, `${parsed.name}${extension}`); } const KNOWN_FILE_EXTENSION_PATTERN = /\.(?:d\.(?:mts|cts|ts)|mts|cts|tsx|ts|mjs|cjs|jsx|js|json)$/i; function hasKnownFileExtension(rawPath) { return KNOWN_FILE_EXTENSION_PATTERN.test(path_1.default.basename(rawPath)); } function resolveRelativeSourceCandidate(source, importer) { if (!source.startsWith("./") && !source.startsWith("../")) return null; const importerDir = path_1.default.dirname(importer); const absolute = path_1.default.resolve(importerDir, source); const candidates = hasKnownFileExtension(source) ? [absolute] : [ absolute, `${absolute}.ts`, `${absolute}.tsx`, `${absolute}.mts`, `${absolute}.cts`, `${absolute}.js`, `${absolute}.mjs`, `${absolute}.cjs`, `${absolute}.json`, path_1.default.join(absolute, "index.ts"), path_1.default.join(absolute, "index.tsx"), path_1.default.join(absolute, "index.mts"), path_1.default.join(absolute, "index.cts"), path_1.default.join(absolute, "index.js"), path_1.default.join(absolute, "index.mjs"), path_1.default.join(absolute, "index.cjs"), path_1.default.join(absolute, "index.json"), ]; for (const candidate of candidates) { try { if (fs_1.default.existsSync(candidate) && fs_1.default.statSync(candidate).isFile()) { return candidate; } } catch { // ignore and continue probing other candidates } } return null; } /** * @description A command-line script for building and bundling TypeScript projects. * @summary This class provides a comprehensive build script that handles TypeScript compilation, * bundling with Rollup, and documentation generation. It supports different build modes * (development, production), module formats (CJS, ESM), and can be extended with custom * configurations. * @class BuildScripts */ class BuildScripts extends command_js_1.Command { constructor() { super("BuildScripts", Object.assign({}, constants_js_1.DefaultCommandOptions, options)); this.replacements = {}; const pkg = (0, index_js_1.getPackage)(); const { name, version } = pkg; this.pkgName = name.includes("@") ? name.split("/")[1] : name; this.pkgVersion = version; try { this.commitHash = (0, child_process_1.execSync)("git rev-parse --short HEAD", { encoding: "utf8", }).trim(); } catch { this.commitHash = "unknown"; } this.fullVersion = `${this.pkgVersion}-${this.commitHash}`; this.replacements[VERSION_STRING] = this.pkgVersion; this.replacements[COMMIT_STRING] = this.commitHash; this.replacements[FULL_VERSION_STRING] = this.fullVersion; this.replacements[PACKAGE_STRING] = name; } /** * @description Patches files with version, commit, full version, and package name. * @summary This method reads all files in a directory, finds placeholders for version, * commit hash, full version, and package name, and replaces them with the actual values * from package.json and the current git revision. * @param {string} p - The path to the directory containing the files to patch. */ patchFiles(p) { const log = this.log.for(this.patchFiles); const { name, version } = (0, index_js_1.getPackage)(); log.info(`Patching ${name} ${version} module in ${p}...`); const stat = fs_1.default.statSync(p); const patchVersionAndPackage = (content) => { let patched = content; // Patch public VERSION assignments without mutating internal VERSION_STRING constants. patched = patched.replace(/((?:^|[\s;,(])(?:const|let|var)\s+VERSION\s*=\s*["'])##VERSION##(["'])/gm, `$1${version}$2`); patched = patched.replace(/((?:^|[\s;,(])(?:const|let|var)\s+COMMIT\s*=\s*["'])##COMMIT##(["'])/gm, `$1${this.commitHash}$2`); patched = patched.replace(/((?:^|[\s;,(])(?:const|let|var)\s+FULL_VERSION\s*=\s*["'])##FULL_VERSION##(["'])/gm, `$1${this.fullVersion}$2`); patched = patched.replace(/((?:^|[\s;,(])(?:exports|module\.exports)\.VERSION\s*=\s*["'])##VERSION##(["'])/gm, `$1${version}$2`); patched = patched.replace(/((?:^|[\s;,(])(?:exports|module\.exports)\.COMMIT\s*=\s*["'])##COMMIT##(["'])/gm, `$1${this.commitHash}$2`); patched = patched.replace(/((?:^|[\s;,(])(?:exports|module\.exports)\.FULL_VERSION\s*=\s*["'])##FULL_VERSION##(["'])/gm, `$1${this.fullVersion}$2`); patched = patched.replace(/((?:^|[\s;,(])\w+\.VERSION\s*=\s*["'])##VERSION##(["'])/gm, `$1${version}$2`); patched = patched.replace(/((?:^|[\s;,(])\w+\.COMMIT\s*=\s*["'])##COMMIT##(["'])/gm, `$1${this.commitHash}$2`); patched = patched.replace(/((?:^|[\s;,(])\w+\.FULL_VERSION\s*=\s*["'])##FULL_VERSION##(["'])/gm, `$1${this.fullVersion}$2`); patched = patched.replace(/((?:^|[\s;,(])(?:const|let|var)\s+PACKAGE_NAME\s*=\s*["'])##PACKAGE##(["'])/gm, `$1${name}$2`); patched = patched.replace(/((?:^|[\s;,(])(?:exports|module\.exports)\.PACKAGE_NAME\s*=\s*["'])##PACKAGE##(["'])/gm, `$1${name}$2`); patched = patched.replace(/((?:^|[\s;,(])\w+\.PACKAGE_NAME\s*=\s*["'])##PACKAGE##(["'])/gm, `$1${name}$2`); return patched; }; if (stat.isDirectory()) fs_1.default.readdirSync(p, { withFileTypes: true, recursive: true }) .filter((p) => p.isFile()) .forEach((file) => { const filePath = path_1.default.join(file.parentPath, file.name); const content = fs_1.default.readFileSync(filePath, "utf8"); const patched = patchVersionAndPackage(content); if (patched !== content) fs_1.default.writeFileSync(filePath, patched, "utf8"); (0, index_js_1.patchFile)(filePath, Object.entries(this.replacements).reduce((acc, [key, val]) => { if ([ VERSION_STRING, COMMIT_STRING, FULL_VERSION_STRING, PACKAGE_STRING, ].includes(key)) return acc; acc[key] = val; return acc; }, {})); }); log.verbose(`Module ${name} ${version} patched in ${p}...`); } reportDiagnostics(diagnostics, logLevel) { const msg = this.formatDiagnostics(diagnostics); try { this.log[logLevel](msg); } catch (e) { console.warn(`Failed to get logger for ${logLevel}`); throw e; } return msg; } // Format diagnostics into a single string for throwing or logging formatDiagnostics(diagnostics) { return diagnostics .map((diagnostic) => { let message = ""; if (diagnostic.file && diagnostic.start) { const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); message += `${diagnostic.file.fileName} (${line + 1},${character + 1})`; } message += ": " + ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"); return message; }) .join("\n"); } readConfigFile(configFileName) { // Read config file const configFileText = fs_1.default.readFileSync(configFileName).toString(); // Parse JSON, after removing comments. Just fancier JSON.parse const result = ts.parseConfigFileTextToJson(configFileName, configFileText); const configObject = result.config; if (!configObject) { this.reportDiagnostics([result.error], logging_1.LogLevel.error); } // Extract config infromation const configParseResult = ts.parseJsonConfigFileContent(configObject, ts.sys, path_1.default.dirname(configFileName)); if (configParseResult.errors.length > 0) this.reportDiagnostics(configParseResult.errors, logging_1.LogLevel.error); return configParseResult; } evalDiagnostics(diagnostics) { if (diagnostics && diagnostics.length > 0) { const errors = diagnostics.filter((d) => d.category === ts.DiagnosticCategory.Error); const warnings = diagnostics.filter((d) => d.category === ts.DiagnosticCategory.Warning); const suggestions = diagnostics.filter((d) => d.category === ts.DiagnosticCategory.Suggestion); const messages = diagnostics.filter((d) => d.category === ts.DiagnosticCategory.Message); // Log diagnostics to console if (warnings.length) this.reportDiagnostics(warnings, logging_1.LogLevel.warn); if (errors.length) { this.reportDiagnostics(diagnostics, logging_1.LogLevel.error); throw new Error(`TypeScript reported ${diagnostics.length} diagnostic(s) during check; aborting.`); } if (suggestions.length) this.reportDiagnostics(suggestions, logging_1.LogLevel.info); if (messages.length) this.reportDiagnostics(messages, logging_1.LogLevel.info); } } preCheckDiagnostics(program) { const diagnostics = ts.getPreEmitDiagnostics(program); this.evalDiagnostics(diagnostics); } // Create a TypeScript program for the current tsconfig and fail if there are any error diagnostics. async checkTsDiagnostics(isDev, mode, bundle = false) { const log = this.log.for(this.checkTsDiagnostics); let tsConfig; try { tsConfig = this.readConfigFile("./tsconfig.json"); } catch (e) { throw new Error(`Failed to parse tsconfig.json: ${e}`); } this.applyTsConfigProfile(tsConfig.options, bundle ? TsBuildTarget.BUNDLE : mode === Modes.ESM ? TsBuildTarget.ESM : TsBuildTarget.CJS_CHECK, isDev); const program = ts.createProgram(tsConfig.fileNames, tsConfig.options); this.preCheckDiagnostics(program); log.verbose(`TypeScript checks passed (${bundle ? "bundle" : "normal"} mode).`); } async buildTs(isDev, mode, bundle = false) { const log = this.log.for(this.buildTs); log.info(`Building ${this.pkgName} ${this.pkgVersion} module (${mode}) in ${isDev ? "dev" : "prod"} mode...`); let tsConfig; try { tsConfig = this.readConfigFile("./tsconfig.json"); } catch (e) { throw new Error(`Failed to parse tsconfig.json: ${e}`); } this.applyTsConfigProfile(tsConfig.options, bundle ? TsBuildTarget.BUNDLE : mode === Modes.ESM ? TsBuildTarget.ESM : TsBuildTarget.CJS_CHECK, isDev); // For production builds we still keep TypeScript comments (removeComments=false in tsconfig) // Bundler/terser will strip comments for production bundles as requested. const program = ts.createProgram(tsConfig.fileNames, tsConfig.options); const transformations = {}; if (mode === Modes.ESM) { transformations.before = [cjs2Transformer(".js")]; } const emitResult = program.emit(undefined, undefined, undefined, undefined, transformations); const allDiagnostics = ts .getPreEmitDiagnostics(program) .concat(emitResult.diagnostics); this.evalDiagnostics(allDiagnostics); } async build(isDev, mode, bundle = false) { const log = this.log.for(this.build); await this.buildTs(isDev, mode, bundle); log.verbose(`Module ${this.pkgName} ${this.pkgVersion} (${mode}) built in ${isDev ? "dev" : "prod"} mode...`); if (mode === Modes.CJS && !bundle) await this.buildCjsFromEsm(isDev); } rewriteRelativeJsSpecifiersToCjs(content) { const replaceSpecifier = (specifier) => { if (!specifier.startsWith("./") && !specifier.startsWith("../") && !specifier.startsWith("/")) return specifier; if (specifier.endsWith(".cjs")) return specifier; if (specifier.endsWith(".js")) return specifier.replace(/\.js$/, ".cjs"); return specifier; }; const quotedSpecifierRegex = /(["'])(\.{1,2}\/[^"']+?)(\1)/g; return content.replace(quotedSpecifierRegex, (_full, quote, specifier, endQuote) => `${quote}${replaceSpecifier(specifier)}${endQuote}`); } async buildCjsFromEsm(isDev) { const log = this.log.for(this.buildCjsFromEsm); log.info(`Building ${this.pkgName} ${this.pkgVersion} module (${Modes.CJS}) from ESM output in ${isDev ? "dev" : "prod"} mode...`); const esmRoot = path_1.default.resolve("lib/esm"); const cjsRoot = path_1.default.resolve("lib/cjs"); fs_1.default.mkdirSync(cjsRoot, { recursive: true }); const esmJsFiles = (0, index_js_1.getAllFiles)(esmRoot, (file) => file.endsWith(".js") && !file.endsWith(".d.js")); for (const file of esmJsFiles) { const relative = path_1.default.relative(esmRoot, file); const outFile = withExtension(path_1.default.join(cjsRoot, relative), ".cjs"); fs_1.default.mkdirSync(path_1.default.dirname(outFile), { recursive: true }); const source = fs_1.default.readFileSync(file, "utf8"); const transpiled = ts.transpileModule(source, { compilerOptions: { module: typescript_1.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, sourceMap: !isDev, inlineSourceMap: isDev, inlineSources: isDev, esModuleInterop: true, }, fileName: relative, reportDiagnostics: true, }); if (transpiled.diagnostics?.length) { this.evalDiagnostics(transpiled.diagnostics); } const mapFileName = `${path_1.default.basename(outFile)}.map`; let rewritten = this.rewriteRelativeJsSpecifiersToCjs(transpiled.outputText); if (transpiled.sourceMapText) { rewritten = rewritten.replace(/\/\/# sourceMappingURL=[^\n]*(\n?)$/, (_match, trailingNewline) => `//# sourceMappingURL=${mapFileName}${trailingNewline}`); } fs_1.default.writeFileSync(outFile, rewritten, "utf8"); if (transpiled.sourceMapText) { const map = JSON.parse(transpiled.sourceMapText); map.file = withExtension(relative, ".cjs"); map.sources = [relative]; fs_1.default.writeFileSync(`${outFile}.map`, `${JSON.stringify(map)}\n`, "utf8"); } } } applyTsConfigProfile(options, target, isDev) { options.declaration = false; options.emitDeclarationOnly = false; options.noEmit = false; options.outFile = undefined; options.moduleResolution = typescript_1.ModuleResolutionKind.Bundler; switch (target) { case TsBuildTarget.ESM: options.module = typescript_1.ModuleKind.ESNext; options.outDir = "lib/esm"; break; case TsBuildTarget.CJS_CHECK: options.module = typescript_1.ModuleKind.Preserve ?? typescript_1.ModuleKind.ESNext; options.moduleResolution = typescript_1.ModuleResolutionKind.Bundler; options.noEmit = true; options.outDir = undefined; break; case TsBuildTarget.TYPES: options.module = typescript_1.ModuleKind.ESNext; options.outDir = "lib/types"; options.declaration = true; options.emitDeclarationOnly = true; break; case TsBuildTarget.NODE_NEXT_VALIDATE: options.module = typescript_1.ModuleKind.NodeNext; options.moduleResolution = typescript_1.ModuleResolutionKind.NodeNext; options.noEmit = true; break; case TsBuildTarget.BUNDLE: options.module = typescript_1.ModuleKind.ESNext; options.moduleResolution = typescript_1.ModuleResolutionKind.Bundler; options.outDir = "dist"; options.isolatedModules = false; options.outFile = undefined; break; } if (target === TsBuildTarget.NODE_NEXT_VALIDATE) { options.inlineSourceMap = false; options.inlineSources = false; options.sourceMap = false; return; } if (isDev) { options.inlineSourceMap = true; options.inlineSources = true; options.sourceMap = false; } else { options.inlineSourceMap = false; options.inlineSources = false; options.sourceMap = true; } } async checkNodeNextCompatibility() { const log = this.log.for(this.checkNodeNextCompatibility); let tsConfig; try { tsConfig = this.readConfigFile("./tsconfig.json"); } catch (e) { throw new Error(`Failed to parse tsconfig.json: ${e}`); } this.applyTsConfigProfile(tsConfig.options, TsBuildTarget.NODE_NEXT_VALIDATE, false); const program = ts.createProgram(tsConfig.fileNames, tsConfig.options); this.preCheckDiagnostics(program); log.verbose("TypeScript NodeNext compatibility check passed."); } async buildTypes(isDev) { const log = this.log.for(this.buildTypes); log.info(`Building ${this.pkgName} ${this.pkgVersion} declaration files...`); let tsConfig; try { tsConfig = this.readConfigFile("./tsconfig.json"); } catch (e) { throw new Error(`Failed to parse tsconfig.json: ${e}`); } this.applyTsConfigProfile(tsConfig.options, TsBuildTarget.TYPES, isDev); const program = ts.createProgram(tsConfig.fileNames, tsConfig.options); const emitResult = program.emit(); const allDiagnostics = ts .getPreEmitDiagnostics(program) .concat(emitResult.diagnostics); this.evalDiagnostics(allDiagnostics); this.emitDualDeclarationFiles(); this.removeLegacyDeclarationFiles(); this.updatePackageJsonDualTypeExports(); } rewriteRelativeDeclarationSpecifiers(content, declarationExtension, sourceFilePath) { const sourceDir = path_1.default.dirname(sourceFilePath); const withDeclarationSpecifier = (specifier) => { if (!specifier.startsWith("./") && !specifier.startsWith("../") && !specifier.startsWith("/")) return specifier; if (/\.(d\.(mts|cts)|mts|cts|ts|json)$/i.test(specifier)) return specifier; const resolved = path_1.default.resolve(sourceDir, specifier); try { if (fs_1.default.existsSync(resolved) && fs_1.default.statSync(resolved).isDirectory()) { return `${specifier}/index${declarationExtension}`; } } catch { // ignore and fallback to file specifier } return `${specifier}${declarationExtension}`; }; let updated = content.replace(/(\b(?:import|export)\b[\s\S]*?\bfrom\s*["'])([^"']+)(["'])/gm, (_full, prefix, specifier, suffix) => `${prefix}${withDeclarationSpecifier(specifier)}${suffix}`); updated = updated.replace(/(\bimport\s*\(\s*["'])([^"']+)(["']\s*\))/gm, (_full, prefix, specifier, suffix) => `${prefix}${withDeclarationSpecifier(specifier)}${suffix}`); updated = updated.replace(/(\brequire\s*\(\s*["'])([^"']+)(["']\s*\))/gm, (_full, prefix, specifier, suffix) => `${prefix}${withDeclarationSpecifier(specifier)}${suffix}`); return updated; } emitDualDeclarationFiles() { const log = this.log.for(this.emitDualDeclarationFiles); const typesRoot = path_1.default.resolve("lib/types"); if (!fs_1.default.existsSync(typesRoot)) return; const typeFiles = (0, index_js_1.getAllFiles)(typesRoot, (file) => file.endsWith(".d.ts")); for (const dtsFile of typeFiles) { const content = fs_1.default.readFileSync(dtsFile, "utf8"); const dMts = dtsFile.replace(/\.d\.ts$/i, ".d.mts"); const dCts = dtsFile.replace(/\.d\.ts$/i, ".d.cts"); fs_1.default.writeFileSync(dMts, this.rewriteRelativeDeclarationSpecifiers(content, ".d.mts", dtsFile), "utf8"); fs_1.default.writeFileSync(dCts, this.rewriteRelativeDeclarationSpecifiers(content, ".d.cts", dtsFile), "utf8"); } log.verbose(`Generated ${typeFiles.length * 2} dual declaration files.`); } removeLegacyDeclarationFiles() { const log = this.log.for(this.removeLegacyDeclarationFiles); const typesRoot = path_1.default.resolve("lib/types"); if (!fs_1.default.existsSync(typesRoot)) return; const legacyFiles = (0, index_js_1.getAllFiles)(typesRoot, (file) => file.endsWith(".d.ts") || file.endsWith(".d.ts.map")); for (const legacyFile of legacyFiles) { try { fs_1.default.unlinkSync(legacyFile); } catch { // ignore stale or already-removed files } } log.verbose(`Removed ${legacyFiles.length} legacy declaration files.`); } updatePackageJsonDualTypeExports() { const log = this.log.for(this.updatePackageJsonDualTypeExports); const packageJsonPath = path_1.default.resolve("package.json"); if (!fs_1.default.existsSync(packageJsonPath)) return; const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf8")); const exportsField = pkg?.exports; if (!exportsField || typeof exportsField !== "object") return; const toDualTypePath = (typesPath, ext) => typesPath.replace(/\.d\.(ts|mts|cts)$/i, ext); const esmToCjsRuntimePath = (runtimePath) => { if (!runtimePath) return undefined; if (runtimePath.includes("/lib/esm/")) { return runtimePath .replace("/lib/esm/", "/lib/cjs/") .replace(/\.js$/i, ".cjs"); } return runtimePath; }; const esmToTypesPath = (runtimePath, ext = ".d.mts") => { if (!runtimePath) return undefined; if (runtimePath.includes("/lib/esm/")) { return runtimePath .replace("/lib/esm/", "/lib/types/") .replace(/\.js$/i, ext); } return undefined; }; const getDefaultEntry = (value) => { if (typeof value === "string") return value; if (value && typeof value === "object" && typeof value.default === "string") { return value.default; } return undefined; }; const getTypesEntry = (value) => { if (value && typeof value === "object" && typeof value.types === "string") { return value.types; } return undefined; }; const updatedExports = {}; for (const [subpath, target] of Object.entries(exportsField)) { if (!target || typeof target !== "object" || Array.isArray(target)) { updatedExports[subpath] = target; continue; } const targetObj = target; const importEntry = getDefaultEntry(targetObj.import); const requireEntryRaw = getDefaultEntry(targetObj.require); const requireEntry = requireEntryRaw && requireEntryRaw.includes("/lib/esm/") ? esmToCjsRuntimePath(requireEntryRaw) : requireEntryRaw || esmToCjsRuntimePath(importEntry); const defaultEntry = getDefaultEntry(targetObj.default); const rootTypes = (typeof targetObj.types === "string" ? targetObj.types : undefined) || getTypesEntry(targetObj.import); const esmTypes = rootTypes && /\.d\.(ts|mts|cts)$/i.test(rootTypes) ? toDualTypePath(rootTypes, ".d.mts") : getTypesEntry(targetObj.import) || esmToTypesPath(importEntry, ".d.mts"); const cjsTypes = rootTypes && /\.d\.(ts|mts|cts)$/i.test(rootTypes) ? toDualTypePath(rootTypes, ".d.cts") : getTypesEntry(targetObj.require) || esmToTypesPath(importEntry, ".d.cts"); updatedExports[subpath] = { ...(importEntry ? { import: { ...(esmTypes ? { types: esmTypes } : {}), default: importEntry, }, } : {}), ...(requireEntry ? { require: { ...(cjsTypes ? { types: cjsTypes } : {}), default: requireEntry, }, } : {}), ...(defaultEntry || importEntry ? { default: defaultEntry || importEntry } : {}), }; } pkg.exports = updatedExports; if (typeof pkg.types === "string" && /\.d\.(ts|mts|cts)$/i.test(pkg.types)) { pkg.types = pkg.types.replace(/\.d\.(ts|mts|cts)$/i, ".d.mts"); } fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2) + "\n", "utf8"); log.verbose("Updated package.json exports with import/require type conditions."); } /** * @description Copies assets to the build output directory. * @summary This method checks for the existence of an 'assets' directory in the source * and copies it to the appropriate build output directory (lib or dist). * @param {Modes} mode - The build mode (CJS or ESM). */ copyAssets(mode) { const log = this.log.for(this.copyAssets); let hasAssets = false; try { hasAssets = fs_1.default.statSync("./src/assets").isDirectory(); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { return log.verbose(`No assets found in ./src/assets to copy`); } if (hasAssets) (0, index_js_1.copyFile)("./src/assets", `./${mode === Modes.CJS ? "lib" : "dist"}/assets`); } /** * @description Bundles the project using Rollup. * @summary This method configures and runs Rollup to bundle the project. It handles * different module formats, development and production builds, and external dependencies. * @param {Modes} mode - The module format (CJS or ESM). * @param {boolean} isDev - Whether it's a development build. * @param {boolean} isLib - Whether it's a library build. * @param {string} [entryFile="src/index.ts"] - The entry file for the bundle. * @param {string} [nameOverride=this.pkgName] - The name of the output bundle. * @param {string|string[]} [externalsArg] - A list of external dependencies. * @param {string|string[]} [includeArg] - A list of dependencies to include. * @returns {Promise<void>} */ async bundle(mode, isDev, isLib, entryFile = "./src/index.ts", nameOverride = this.pkgName, externalsArg, includeArg = [ "prompts", "styled-string-builder", "typed-object-accumulator", "@decaf-ts/logging", ]) { // Run a TypeScript-only diagnostic check for the bundling configuration and fail fast on any errors. await this.checkTsDiagnostics(isDev, mode, true); const isEsm = mode === Modes.ESM; const pkgName = this.pkgName; const log = this.log; // normalize include and externals const include = Array.from(new Set([...parseList(includeArg)])); let externalsList = parseList(externalsArg); if (externalsList.length === 0) { // if no externals specified, list top-level packages in node_modules (expand scopes) try { externalsList = (0, index_js_1.listNodeModulesPackages)(path_1.default.join(process.cwd(), "node_modules")); } catch { // fallback to package.json dependencies if listing fails or yields nothing } if (!externalsList || externalsList.length === 0) { externalsList = getPackageDependencies(); } } const ext = Array.from(new Set([ // builtins and always external runtime deps ...(function builtinList() { try { return (Array.isArray(module_1.builtinModules) ? module_1.builtinModules : []); } catch { // fallback to a reasonable subset if `builtinModules` is unavailable return [ "fs", "path", "process", "child_process", "util", "https", "http", "os", "stream", "crypto", "zlib", "net", "tls", "url", "querystring", "assert", "events", "tty", "dns", "querystring", ]; } })(), ...externalsList, ])); // For plugin-typescript we want it to emit source maps (not inline) so Rollup can // decide whether to inline or emit external files. The Rollup output.sourcemap // controls final map placement. Do NOT set a non-standard `sourcemap` field on // the rollup input options (Rollup will reject it). const rollupSourceMapOutput = isDev ? "inline" : true; const plugins = [ { name: "resolve-dotted-relative-imports", resolveId(source, importer) { if (!importer) return null; return resolveRelativeSourceCandidate(source, importer); }, }, (0, plugin_typescript_1.default)({ compilerOptions: { module: "esnext", declaration: false, outDir: isLib ? "bin" : "dist", // For dev bundles emit inline source maps (no separate .map files). // For prod bundles emit external maps so Rollup can write them to disk. sourceMap: isDev ? false : true, inlineSourceMap: isDev ? true : false, inlineSources: isDev ? true : false, }, include: ["src/**/*.ts"], exclude: ["node_modules", "**/*.spec.ts"], tsconfig: "./tsconfig.json", }), (0, plugin_json_1.default)(), ]; if (isLib) { plugins.push((0, plugin_commonjs_1.default)({ include: [], exclude: externalsList, }), (0, plugin_node_resolve_1.nodeResolve)({ extensions: [ ".mjs", ".js", ".json", ".node", ".ts", ".mts", ".cts", ".tsx", ], resolveOnly: include, })); } // production minification: add terser last so it sees prior source maps try { const terserMod = await Promise.resolve().then(() => __importStar(require("@rollup/plugin-terser"))); const terserFn = (terserMod && terserMod.terser) || terserMod.default || terserMod; const terserOptionsDev = { parse: { ecma: 2020 }, compress: false, mangle: false, format: { comments: false, beautify: true, }, }; const terserOptionsProd = { parse: { ecma: 2020 }, compress: { ecma: 2020, passes: 5, drop_console: true, drop_debugger: true, toplevel: true, module: isEsm, unsafe: true, unsafe_arrows: true, unsafe_comps: true, collapse_vars: true, reduce_funcs: true, reduce_vars: true, }, mangle: { toplevel: true, }, format: { comments: false, ascii_only: true, }, toplevel: true, }; plugins.push(terserFn(isDev ? terserOptionsDev : terserOptionsProd)); } catch { // if terser isn't available, ignore } const input = { input: entryFile, plugins: plugins, external: ext, onwarn: undefined, // enable tree-shaking for production bundles treeshake: !isDev, }; // prepare output globals mapping for externals const globals = {}; // include all externals and builtins (ext) so Rollup won't guess names for builtins ext.forEach((e) => { globals[e] = packageToGlobal(e); }); const outputDir = isLib ? "bin" : "dist"; const entryFileName = `${nameOverride ? nameOverride : `.bundle.${!isDev ? "min" : ""}`}${isEsm ? ".js" : ".cjs"}`; const outputs = [ { dir: outputDir, entryFileNames: entryFileName, format: isLib ? "cjs" : isEsm ? "esm" : "umd", name: pkgName, esModule: isEsm, // output sourcemap: inline for dev, external for prod sourcemap: rollupSourceMapOutput, globals: globals, exports: "auto", }, ]; try { const { rollup } = (await Promise.resolve().then(() => __importStar(require("rollup")))); const bundle = await rollup(input); // only log watchFiles at verbose level to avoid noisy console output log.verbose(bundle.watchFiles); async function generateOutputs(bundle) { for (const outputOptions of outputs) { await bundle.write(outputOptions); } } try { await generateOutputs(bundle); } finally { await bundle.close(); } } catch (e) { throw new Error(`Failed to bundle: ${e}`); } } async buildByEnv(entryFile = "./src/index.ts", isDev, mode = BuildMode.ALL, validateNodeNext = false, includesArg, externalsArg) { if (validateNodeNext) await this.checkNodeNextCompatibility(); // note: includes and ext