UNPKG

@aptrn/maxmsp-ts

Version:

CLI tool for building typescript projects and dependencies for usage in MaxMsp js object

313 lines (312 loc) 12.6 kB
import { spawn } from "child_process"; import * as fs from "fs/promises"; import * as path from "path"; import chokidar from "chokidar"; const [, , command, ...args] = process.argv; const configFilePath = path.join(process.cwd(), "maxmsp.config.json"); // Function to sanitize library names function sanitizeName(name) { return name.replace(/[/.]/g, "-"); // Replace / and . with - } // Function to read or create the config file async function readOrCreateConfig() { try { await fs.access(configFilePath); // Check if the config file exists const configContent = await fs.readFile(configFilePath, "utf-8"); return JSON.parse(configContent); // Cast to Config type } catch { const initialConfig = { output_path: "lib", dependencies: {}, }; await fs.writeFile(configFilePath, JSON.stringify(initialConfig, null, 2)); console.log("maxmsp.config.json created."); return initialConfig; } } // Function to save the config file async function saveConfig(config) { await fs.writeFile(configFilePath, JSON.stringify(config, null, 2)); } // Function to copy and rename files for dependencies async function copyAndRenameFiles(src, dest, alias, files) { try { // Create the full destination path based on tsconfig output dir and relative path const targetDir = dest; // Use dest directly, which includes the relative path await fs.mkdir(targetDir, { recursive: true }); for (let file of files) { const srcPath = path.join(src, file); const newName = `${alias}_${file}`; // Prepend alias to the original file name const destPath = path.join(targetDir, newName); // Use targetDir await fs.copyFile(srcPath, destPath); console.log(`Copied ${srcPath} to ${destPath}`); } } catch (error) { console.error(`Error copying files from ${src} to ${dest}:`, error); } } // Function to replace require statements in a file async function replaceInFile(filePath, alias, packageName, effectivePath) { try { let content = await fs.readFile(filePath, "utf8"); // Construct the new require path based on alias and effectivePath const newRequirePath = `require("lib/${effectivePath}/${alias}_index.js")`; // Replace require statements for the specific packageName content = content.replace(new RegExp(`require\\("${packageName}"\\)`, "g"), newRequirePath); await fs.writeFile(filePath, content, "utf8"); console.log(`Updated require statements in ${filePath}`); } catch (error) { console.error(`Error processing file ${filePath}:`, error); } } // Function to get output directory from tsconfig.json async function getTsConfigOutputDir() { const tsConfigPath = path.join(process.cwd(), "tsconfig.json"); try { const tsConfigContent = await fs.readFile(tsConfigPath, "utf-8"); const tsConfig = JSON.parse(tsConfigContent); // Return outDir if it exists, otherwise return a default value return tsConfig.compilerOptions?.outDir || "dist"; // Default to "dist" if not specified } catch (error) { console.error(`Error reading tsconfig.json:`, error); return "dist"; // Fallback output directory } } // Post-build command logic async function postBuild() { const config = await readOrCreateConfig(); // Get the output directory from tsconfig.json const tsConfigOutputDir = await getTsConfigOutputDir(); // Define output directory based on config const outputDir = path.join(tsConfigOutputDir, config.output_path); // Full path for output for (const [packageName, { alias, files, path: relativePath },] of Object.entries(config.dependencies)) { // Use libraryName if alias is empty const effectiveAlias = alias || sanitizeName(packageName); // Sanitize if alias is empty // Use sanitized library name if path is empty, otherwise use provided path const effectivePath = relativePath ? relativePath : sanitizeName(packageName); const sourceDir = path.join(process.cwd(), "node_modules", packageName, "dist"); // Construct target directory based on tsconfig output dir and effectivePath const targetDir = path.join(outputDir, effectivePath); // e.g., tsconfigOutputDir/lib/lol // Copy and rename files for each dependency await copyAndRenameFiles(sourceDir, targetDir, effectiveAlias, files); // Replace require statements in all JavaScript files except those in the lib folder await replaceRequireStatements(tsConfigOutputDir, packageName, effectiveAlias, effectivePath); } console.log("Post-build completed successfully."); } // Function to replace require statements in JavaScript files async function replaceRequireStatements(baseDir, packageName, alias, effectivePath) { try { const libFolder = path.join(baseDir, "lib"); // Read all JavaScript files recursively from baseDir const jsFiles = await getAllJsFiles(baseDir); for (const filePath of jsFiles) { if (!filePath.startsWith(libFolder)) { // Skip any file in the lib folder console.log(`Updating require statements in ${filePath}`); await replaceInFile(filePath, alias, packageName, effectivePath); // Pass effectivePath for replacement } } } catch (error) { console.error(`Error replacing require statements:`, error); } } // Function to get all JavaScript files recursively from a directory async function getAllJsFiles(dir) { let results = []; const list = await fs.readdir(dir); for (const file of list) { const filePath = path.join(dir, file); const stat = await fs.stat(filePath); if (stat && stat.isDirectory()) { results = results.concat(await getAllJsFiles(filePath)); // Recurse into subdirectory } else if (path.extname(file) === ".js") { results.push(filePath); // Add JS file to results } } return results; } // Init command logic async function init() { await readOrCreateConfig(); } // Function to check if a library is installed async function isLibraryInstalled(libraryName) { const packageJsonPath = path.join(process.cwd(), "package.json"); try { const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8"); const packageJson = JSON.parse(packageJsonContent); // Check both dependencies and devDependencies return ((packageJson.dependencies && packageJson.dependencies[libraryName]) || (packageJson.devDependencies && packageJson.devDependencies[libraryName])); } catch (error) { console.error(`Error reading package.json:`, error); return false; } } // Add command logic async function add(libraryName, options) { const config = await readOrCreateConfig(); // Check if the library is installed const isInstalled = await isLibraryInstalled(libraryName); if (!isInstalled) { console.error(`${libraryName} is not installed. Please install it with pnpm i -D ${libraryName}`); process.exit(1); } // Default values for the new dependency const alias = options.alias || libraryName; // Process the files option if provided let files = []; if (typeof options.files === "string") { files = options.files.split(",").map((file) => file.trim()); } else { files = ["index.js"]; } const pathValue = options.path || ""; // Add dependency config.dependencies[libraryName] = { alias, files, path: pathValue }; await saveConfig(config); console.log(`Added dependency ${libraryName} with alias ${alias}.`); } // Remove command logic async function remove(libraryName) { const config = await readOrCreateConfig(); // Remove dependency if it exists if (config.dependencies[libraryName]) { delete config.dependencies[libraryName]; await saveConfig(config); console.log(`Removed dependency ${libraryName}.`); } else { console.error(`Dependency ${libraryName} not found.`); } } // Build command logic async function build() { return new Promise((resolve, reject) => { const tsc = spawn(process.platform === "win32" ? "npx.cmd" : "npx", ["tsc"], { stdio: "inherit", shell: true }); tsc.on("close", async (code) => { if (code !== 0) { reject(new Error(`TypeScript compilation failed with code ${code}`)); return; } try { await postBuild(); resolve(true); } catch (postBuildError) { reject(postBuildError); } }); tsc.on("error", (error) => reject(error)); }); } // Dev command logic with file watching async function dev() { const srcDir = path.join(process.cwd(), "src"); console.log("Starting watch mode in " + srcDir); // Watch for changes in .ts and .json files within src directory const watcher = chokidar.watch(srcDir, { ignored: /(^|[\/\\])\../, // Ignore dotfiles persistent: true, usePolling: true, // Enable polling interval: 100, // Polling interval in milliseconds }); watcher.on("ready", () => { console.log("Initial scan complete. Watching for file changes..."); // Initial build on start build().catch((err) => console.error("Initial build failed:", err.message)); }); watcher.on("change", (filePath) => { console.log(`File ${filePath} has been changed.`); // Run build on change build() .then(() => { console.log(`Build completed successfully after change in ${filePath}`); }) .catch((err) => { console.error(`Build failed after change in ${filePath}:`, err.message); }); }); watcher.on("add", (filePath) => { console.log(`File ${filePath} has been added.`); // Optionally trigger a build if new files are added build() .then(() => { console.log(`Build completed successfully after adding ${filePath}`); }) .catch((err) => { console.error(`Build failed after adding ${filePath}:`, err.message); }); }); watcher.on("unlink", (filePath) => { console.log(`File ${filePath} has been removed.`); // Optionally handle file removal if needed }); watcher.on("error", (error) => { console.error("Watcher error:", error); }); process.on("SIGINT", () => { console.log("Watch mode terminated."); watcher.close(); process.exit(0); }); } // Command handling (async () => { switch (command) { case "build": try { await build(); console.log("Build and post-build completed successfully."); } catch (err) { console.error("Build failed:", err); process.exit(1); } break; case "dev": dev(); // Call the new dev function with watching capability. break; case "init": await init(); break; case "add": const libraryNameToAdd = args[0]; const optionsToAdd = {}; args.forEach((arg, index) => { if (arg === "--alias") optionsToAdd.alias = args[index + 1]; if (arg === "--files") optionsToAdd.files = args[index + 1]; if (arg === "--path") optionsToAdd.path = args[index + 1]; }); if (!libraryNameToAdd) { console.error("Please specify a library name to add."); process.exit(1); } await add(libraryNameToAdd, optionsToAdd); break; case "rm": const libraryNameToRemove = args[0]; if (!libraryNameToRemove) { console.error("Please specify a library name to remove."); process.exit(1); } await remove(libraryNameToRemove); break; default: console.log("Unknown command. Use build, dev, init, add <libraryName>, or rm <libraryName>."); } })();