UNPKG

@decaf-ts/utils

Version:

module management utils for decaf-ts

722 lines 30.7 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_1 = require("./../command.cjs"); const constants_1 = require("./../constants.cjs"); const utils_1 = require("./../../utils/index.cjs"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const rollup_1 = require("rollup"); 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 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, utils_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, utils_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])); } const VERSION_STRING = "##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 = {})); 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, }, banner: { 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); } } 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; // only when path is relative if (!node.moduleSpecifier.text.startsWith("./") && !node.moduleSpecifier.text.startsWith("../")) return false; // only when module specifier has no extension if (path_1.default.extname(node.moduleSpecifier.text) !== "") return false; return true; } return ts.visitNode(sourceFile, visitNode); }; }; }; class BuildScripts extends command_1.Command { constructor() { super("BuildScripts", Object.assign({}, constants_1.DefaultCommandOptions, options)); this.replacements = {}; const pkg = (0, utils_1.getPackage)(); const { name, version } = pkg; this.pkgName = name.includes("@") ? name.split("/")[1] : name; this.pkgVersion = version; this.replacements[VERSION_STRING] = this.pkgVersion; this.replacements[PACKAGE_STRING] = name; } patchFiles(p) { const log = this.log.for(this.patchFiles); const { name, version } = (0, utils_1.getPackage)(); log.info(`Patching ${name} ${version} module in ${p}...`); const stat = fs_1.default.statSync(p); if (stat.isDirectory()) fs_1.default.readdirSync(p, { withFileTypes: true, recursive: true }) .filter((p) => p.isFile()) .forEach((file) => (0, utils_1.patchFile)(path_1.default.join(file.parentPath, file.name), Object.entries(this.replacements).reduce((acc, [key, val]) => { switch (key) { case VERSION_STRING: log.debug("Found VERSION string to replace"); acc[`VERSION = "${VERSION_STRING}";`] = `VERSION = "${val}";`; break; case PACKAGE_STRING: log.debug("Found PACKAGE_NAME string to replace"); acc[`PACKAGE_NAME = "${PACKAGE_STRING}";`] = `PACKAGE_NAME = "${val}";`; break; default: 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}`); } if (bundle) { tsConfig.options.module = typescript_1.ModuleKind.AMD; tsConfig.options.outDir = "dist"; tsConfig.options.isolatedModules = false; tsConfig.options.outFile = this.pkgName; } else { tsConfig.options.outDir = `lib${mode === Modes.ESM ? "/esm" : ""}`; tsConfig.options.module = mode === Modes.ESM ? typescript_1.ModuleKind.ES2022 : typescript_1.ModuleKind.CommonJS; } // Ensure TypeScript emits inline source maps for both dev and prod (bundlers will control external maps) // Keep comments in TS emit by default; bundling/minification will handle removal where requested. // Emit external source maps from TypeScript so editors/debuggers can find them. // Turn off inline maps/sources so bundlers (Rollup) can control whether maps are inlined or written externally. tsConfig.options.inlineSourceMap = false; tsConfig.options.inlineSources = false; tsConfig.options.sourceMap = true; 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}`); } if (bundle) { tsConfig.options.module = typescript_1.ModuleKind.AMD; tsConfig.options.outDir = "dist"; tsConfig.options.isolatedModules = false; tsConfig.options.outFile = this.pkgName; } else { tsConfig.options.outDir = `lib${mode === Modes.ESM ? "/esm" : ""}`; tsConfig.options.module = mode === Modes.ESM ? typescript_1.ModuleKind.ES2022 : typescript_1.ModuleKind.CommonJS; } // Always emit inline source maps from tsc (bundler will emit external maps for production bundles). // For dev builds we want TypeScript to emit inline source maps so no separate .map files are produced. // For production we emit external source maps so the bundler can further transform and emit them. if (isDev) { tsConfig.options.inlineSourceMap = true; tsConfig.options.inlineSources = true; tsConfig.options.sourceMap = false; } else { tsConfig.options.inlineSourceMap = false; tsConfig.options.inlineSources = false; tsConfig.options.sourceMap = true; } // 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.CJS) { transformations.before = [cjs2Transformer(".cjs")]; } else 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) { const files = (0, utils_1.getAllFiles)("lib", (file) => file.endsWith(".js") && !file.includes("/esm/")); for (const file of files) { log.verbose(`Patching ${file}'s cjs imports...`); const f = file.replace(".js", ".cjs"); await (0, utils_1.renameFile)(file, f); } } } 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, utils_1.copyFile)("./src/assets", `./${mode === Modes.CJS ? "lib" : "dist"}/assets`); } 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, utils_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 = [ (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)({ 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 outputs = [ { file: `${isLib ? "bin/" : "dist/"}${nameOverride ? nameOverride : `.bundle.${!isDev ? "min" : ""}`}${isEsm ? ".js" : ".cjs"}`, 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 bundle = await (0, rollup_1.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); } } await generateOutputs(bundle); } catch (e) { throw new Error(`Failed to bundle: ${e}`); } } async buildByEnv(isDev, mode = BuildMode.ALL, includesArg, externalsArg) { // note: includes and externals will be passed through from run() into this method by callers try { (0, utils_1.deletePath)("lib"); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // do nothing } try { (0, utils_1.deletePath)("dist"); // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { // do nothing } fs_1.default.mkdirSync("lib"); fs_1.default.mkdirSync("dist"); if ([BuildMode.ALL, BuildMode.BUILD].includes(mode)) { await this.build(isDev, Modes.ESM); await this.build(isDev, Modes.CJS); this.patchFiles("lib"); } if ([BuildMode.ALL, BuildMode.BUNDLE].includes(mode)) { await this.bundle(Modes.ESM, isDev, false, "src/index.ts", this.pkgName, externalsArg, includesArg); await this.bundle(Modes.CJS, isDev, false, "src/index.ts", this.pkgName, externalsArg, includesArg); this.patchFiles("dist"); } this.copyAssets(Modes.CJS); this.copyAssets(Modes.ESM); } async buildDev(mode = BuildMode.ALL, includesArg, externalsArg) { return this.buildByEnv(true, mode, includesArg, externalsArg); } async buildProd(mode = BuildMode.ALL, includesArg, externalsArg) { return this.buildByEnv(false, mode, includesArg, externalsArg); } async buildDocs() { await (0, utils_1.runCommand)(`npm install better-docs taffydb`).promise; await (0, utils_1.runCommand)(`npx markdown-include ./workdocs/readme-md.json`).promise; await (0, utils_1.runCommand)(`npx jsdoc -c ./workdocs/jsdocs.json -t ./node_modules/better-docs`).promise; await (0, utils_1.runCommand)(`npm remove better-docs taffydb`).promise; [ { src: "workdocs/assets", dest: "./docs/workdocs/assets", }, { src: "workdocs/reports/coverage", dest: "./docs/workdocs/reports/coverage", }, { src: "workdocs/reports/html", dest: "./docs/workdocs/reports/html", }, { src: "workdocs/resources", dest: "./docs/workdocs/resources", }, { src: "LICENSE.md", dest: "./docs/LICENSE.md", }, ].forEach((f) => { const { src, dest } = f; (0, utils_1.copyFile)(src, dest); }); // patch ./README.md file to replace version/package/package size strings try { const sizeKb = await (0, utils_1.getFileSizeZipped)(path_1.default.resolve(path_1.default.join(process.cwd(), "dist"))); this.replacements[PACKAGE_SIZE_STRING] = `${sizeKb} KB`; } catch { // if we couldn't compute size, leave placeholder or set to unknown this.replacements[PACKAGE_SIZE_STRING] = "unknown"; } // Patch README.md in project root try { (0, utils_1.patchFile)("./README.md", this.replacements); } catch (e) { const log = this.log.for(this.buildDocs); log.verbose(`Failed to patch README.md: ${e}`); } } async run(answers) { const { dev, prod, docs, buildMode, includes, externals } = answers; if (dev) { return await this.buildDev(buildMode, includes, externals); } if (prod) { return await this.buildProd(buildMode, includes, externals); } if (docs) { return await this.buildDocs(); } } } exports.BuildScripts = BuildScripts; //# sourceMappingURL=build-scripts.js.map