Last active
May 14, 2024 03:00
-
-
Save mfp22/9777d771d6392b83719608d8eb9553fe to your computer and use it in GitHub Desktop.
node.js script to create Nx library and move folder into it
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fs = require('fs'); | |
const path = require('path'); | |
const exec = require('child_process').exec; | |
const ts = require('typescript'); | |
const tsHost = ts.createCompilerHost( | |
{ | |
allowJs: true, | |
noEmit: true, | |
isolatedModules: true, | |
resolveJsonModule: false, | |
moduleResolution: ts.ModuleResolutionKind.Classic, // we don't want node_modules | |
incremental: true, | |
noLib: true, | |
noResolve: true, | |
}, | |
true | |
); | |
function delintNode(node) { | |
if (!ts.isImportDeclaration(node)) { | |
let froms = []; | |
ts.forEachChild(node, (processedNode) => { | |
froms.push(delintNode(processedNode)); | |
}); | |
return froms.flat(); | |
} | |
const named = node.importClause?.namedBindings?.elements || []; | |
const name = node.importClause?.name?.escapedText; | |
const nname = node.importClause?.namedBindings?.name?.escapedText; | |
const from = node.moduleSpecifier.text; | |
const typeonly = node.importClause?.isTypeOnly; | |
return from; | |
} | |
function getImports(fileName) { | |
const sourceFile = tsHost.getSourceFile( | |
fileName, | |
ts.ScriptTarget.Latest, | |
(msg) => { | |
throw new Error(`Failed to parse ${fileName}: ${msg}`); | |
} | |
); | |
if (!sourceFile) { | |
throw ReferenceError(`Failed to find file ${fileName}`); | |
} | |
return delintNode(sourceFile); | |
} | |
// Capture CLI arguments | |
const args = process.argv.slice(2); // Removes first two elements ("node" and script name) | |
if (args.length !== 3) { | |
console.error( | |
'Usage: node convert-folder-to-library.js "<folder-path>" "<library-name>" "<org-name>"' | |
); | |
process.exit(1); | |
} | |
const [folderPath, libraryName, orgName] = args; | |
// Execute the conversion function | |
convertFolderToLibrary(folderPath, libraryName, orgName).catch(console.error); | |
// Function to execute shell commands | |
function executeCommand(command) { | |
return new Promise((resolve, reject) => { | |
exec(command, (error, stdout, stderr) => { | |
if (error) reject(stderr); | |
else resolve(stdout); | |
}); | |
}); | |
} | |
// Compute the relative import path from the file's directory to the target searchPath | |
function computeRelativeImportPath(fromPath, toPath) { | |
const fromDir = path.dirname(fromPath); | |
// Ensure both paths are absolute for accurate calculation | |
const absoluteFromDir = path.resolve(fromDir); | |
const absoluteToPath = path.resolve(toPath); | |
let relativePath = path.relative(absoluteFromDir, absoluteToPath); | |
if (!relativePath.startsWith('.')) { | |
relativePath = './' + relativePath; | |
} | |
console.log( | |
'absoluteFromDir', | |
absoluteFromDir, | |
'absoluteToPath', | |
absoluteToPath, | |
'relativePath', | |
relativePath | |
); | |
return relativePath; | |
} | |
// Recursive function to find files that import from a specific path | |
function findFilesWithImportPath(basePath, searchPath, excludePath = '') { | |
let filesToUpdate = []; | |
const filesAndFolders = fs.readdirSync(basePath, { withFileTypes: true }); | |
for (const dirent of filesAndFolders) { | |
const fullPath = path.join(basePath, dirent.name); | |
if (!excludePath || fullPath.startsWith(excludePath)) { | |
console.log(`Skipping excluded directory: ${excludePath}`); | |
continue; | |
} | |
if (dirent.isDirectory()) { | |
filesToUpdate = filesToUpdate.concat( | |
findFilesWithImportPath(fullPath, searchPath, excludePath) | |
); | |
} else if (dirent.isFile() && path.extname(dirent.name) === '.ts') { | |
// const fileContent = fs.readFileSync(fullPath, 'utf8'); | |
// console.log('fullPath', fullPath, 'searchPath', searchPath, 'fileContent'); | |
// Calculate the relative import path for this specific file | |
const relativeImportPath = computeRelativeImportPath( | |
fullPath, | |
searchPath | |
); | |
// Check if this specific relative import path is used in the file | |
const importPaths = getImports(fullPath); | |
console.log('importPaths', importPaths); | |
console.log('relativeImportPath', relativeImportPath); | |
if ( | |
importPaths.find((importPath) => | |
importPath.startsWith(relativeImportPath) | |
) | |
) { | |
filesToUpdate.push(fullPath); | |
} | |
} | |
} | |
return filesToUpdate; | |
} | |
// Function to update import paths in a file | |
function updateImportPath(filePath, oldImportPath, newImportPath) { | |
const fileContent = fs.readFileSync(filePath, 'utf8'); | |
// Construct a pattern to match both direct and relative imports | |
const oldImportPattern = new RegExp( | |
`(['"])${oldImportPath}/([^'"]+)(['"])`, | |
'g' | |
); | |
const updatedContent = fileContent.replace( | |
oldImportPattern, | |
`$1${newImportPath}/$2$3` | |
); | |
// Logging for changes made | |
if (fileContent !== updatedContent) { | |
console.log(`Updating import in ${filePath}`); | |
} else { | |
console.log(`No change needed for ${filePath}`); | |
} | |
fs.writeFileSync(filePath, updatedContent); | |
} | |
/** | |
* Adjust external imports in files within a moved directory based on the new location. | |
* | |
* @param {string} oldDir - The original directory path before the move. | |
* @param {string} newDir - The new directory path after the move. | |
*/ | |
function adjustExternalImports(oldDir, newDir) { | |
// Normalize and resolve paths | |
oldDir = path.resolve(oldDir); | |
newDir = path.resolve(newDir); | |
// Calculate the relative path shift needed for the move | |
const relativePathShift = path.relative(newDir, oldDir); | |
// Function to recursively update imports in files, accounting for depth. | |
function updateImportsInFiles(dirPath, depth = 0) { | |
const files = fs.readdirSync(dirPath); | |
for (const file of files) { | |
const fullPath = path.join(dirPath, file); | |
const stats = fs.statSync(fullPath); | |
if (stats.isDirectory()) { | |
updateImportsInFiles(fullPath, depth + 1); | |
} else if (file.endsWith('.js') || file.endsWith('.ts')) { | |
let content = fs.readFileSync(fullPath, 'utf8'); | |
const debugPath = 'products/product.service'; | |
// Adjust only external import paths. | |
content = content.replace( | |
/(import\s+.*?\s+from\s+['"])(.*?)(['"])/g, | |
(match, p1, p2, p3) => { | |
// Count the number of '../' or '..\' navigating out of the current file | |
const upNavigations = | |
(p2.match(/\.\.\//g) || []).length + | |
(p2.match(/\.\.\\/g) || []).length; | |
if (upNavigations > depth) { | |
// Calculate the new path with the relative shift | |
// path.join treats each stop as a folder that would require a '..' to get out of | |
// So wee need path.direname to get the folder, not the file | |
const outOfDir = path.relative(path.dirname(fullPath), newDir); | |
const newDirToOldDir = path.relative(newDir, oldDir); | |
const oldDirToOldPath = path.relative( | |
newDir, | |
path.dirname(fullPath) | |
); | |
const oldPathToTarget = p2; | |
const newImportPath = path.join( | |
outOfDir, | |
newDirToOldDir, | |
oldDirToOldPath, | |
oldPathToTarget | |
); | |
// const targetPath = path.resolve(oldDir, filePathToNewDir, p2); // Full path to the target based on the old directory | |
// const newImportPath = path.relative( | |
// path.dirname(fullPath), | |
// targetPath | |
// ); // Compute new relative path | |
const normalizedNewPath = newImportPath.split(path.sep).join('/'); // Ensure forward slashes for import paths | |
const newImport = `${p1}${normalizedNewPath}${p3}`; | |
console.log('match', match); | |
console.log('p1', p1); | |
console.log('p2', p2); | |
console.log('p3', p3); | |
console.log('upNavigations', upNavigations); | |
console.log('outOfDir', outOfDir); | |
console.log('newDirToOldDir', newDirToOldDir); | |
console.log('oldDirToOldPath', oldDirToOldPath); | |
console.log('oldPathToTarget', oldPathToTarget); | |
// console.log('filePathToNewDir', filePathToNewDir); | |
// console.log('targetPath', targetPath); | |
console.log('newImportPath', newImportPath); | |
console.log('normalizedNewPath', normalizedNewPath); | |
console.log('newImport', newImport); | |
return newImport; | |
} | |
// if (upNavigations > depth) { | |
// // It's external, calculate the new path with the relative shift | |
// console.log( | |
// '===============================================================\n\n===========\n\n===========\n\n\n\n' | |
// ); | |
// const newPath = path.join(relativePathShift, p2); | |
// const normalizedNewPath = newPath.split(path.sep).join('/'); // Ensure forward slashes for import paths | |
// const newImport = `${p1}${normalizedNewPath}${p3}`; | |
// if (content.includes(debugPath)) { | |
// console.log('match', match); | |
// console.log('p1', p1); | |
// console.log('p2', p2); | |
// console.log('p3', p3); | |
// console.log('upNavigations', upNavigations); | |
// console.log('relativePathShift', relativePathShift); | |
// console.log('newPath', newPath); | |
// console.log('normalizedNewPath', normalizedNewPath); | |
// console.log('newImport', newImport); | |
// } | |
// return newImport; | |
// } | |
return match; // Keep internal imports unchanged | |
} | |
); | |
// Write updated content back to the file. | |
fs.writeFileSync(fullPath, content, 'utf8'); | |
console.log(`Updated external imports in: ${fullPath}`); | |
} | |
} | |
} | |
// Initiate the recursive import adjustment. | |
updateImportsInFiles(newDir); | |
} | |
// Main function to convert folder into Nx library | |
async function convertFolderToLibrary(folderPath, libraryName, orgName) { | |
// Step 1: Create the Nx library | |
await executeCommand( | |
`npx nx generate @nx/angular:library --name=${libraryName} --unitTestRunner=jest --directory=src/libs/${libraryName} --importPath=@${orgName}/${libraryName} --projectNameAndRootFormat=as-provided --no-interactive` | |
); | |
// Step 2: Move the folder | |
const librarySrcPath = `src/libs/${libraryName}/src`; | |
await executeCommand(`rm -rf ${librarySrcPath}/lib`); | |
const destinationPath = path.join(librarySrcPath, 'lib'); | |
fs.renameSync(folderPath, destinationPath); | |
// Step 2.1: Adjust imports in files within the moved directory. | |
adjustExternalImports(folderPath, destinationPath); | |
// Step 3: Update import paths in the project | |
const excludePath = path.join('src', 'libs', libraryName); | |
console.log('path.resolve(folderPath)', path.resolve(folderPath)); | |
const filesToUpdate = findFilesWithImportPath( | |
'./src/', | |
path.resolve(folderPath), | |
excludePath | |
); | |
console.log(`Files to update imports: ${filesToUpdate.join(', ')}`); // Log files identified for updating | |
for (const file of filesToUpdate) { | |
console.log( | |
`Updating import in ${file} from ${path.basename( | |
folderPath | |
)} to @${orgName}/${libraryName}` | |
); // Log each update | |
const relativeImportPath = computeRelativeImportPath( | |
file, | |
path.resolve(folderPath) | |
); | |
console.log('relativeImportPath', relativeImportPath); | |
updateImportPath(file, relativeImportPath, `@${orgName}/${libraryName}`); | |
} | |
// Step 4: Update index.ts with exports | |
const filesInLib = fs.readdirSync(destinationPath); | |
const indexTsPath = path.join(librarySrcPath, 'index.ts'); | |
let exportsContent = filesInLib | |
.filter((file) => path.extname(file) === '.ts') | |
.map((file) => `export * from './lib/${path.basename(file, '.ts')}';`) | |
.join('\n'); | |
fs.writeFileSync(indexTsPath, exportsContent); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment