janniks / basetag

βšΎοΈβ€‚A better way to import local NodeJS modules
MIT License
38 stars 3 forks source link

Tool to re-root, de-root existing project. #17

Open coolaj86 opened 3 years ago

coolaj86 commented 3 years ago

I discovered this because I've inherited a project with (literally) hundreds of files, many deeply nested.

(I was searching around a bit after having realized Windows symlinks wouldn't work - not yet knowing about junctions - and stumbled upon a blog that mentioned this while hoping to find an updated 'official' solution)

Anyway, I'm writing a tool that will convert a project's requires over to "the basetag way". It may provide a nice starting point to build something that could be put in this project as a bin script and be able to do something like this:

npx basetag rebase-requires
coolaj86 commented 3 years ago

My first pass:

Usage

Must be run from the package root.

node reroot-requires.js

Example output:

# [ ./src/middleware no-cache.js ]
# $/src/api/util/error.js <= ../api/util/error.js
# $/src/models/user.js <= ../models/user.js
git add src/middleware/no-cache.js ;

# [ ./src/middleware/errors api-error-handler.js ]
# $/src/api/util/error.js <= ../../api/util/error.js
git add src/middleware/errors/api-error-handler.js ;

Source

'use strict';

var path = require('path');
var fs = require('fs').promises;

// assume that the command is run from the package root
var pkglen = process.cwd().length; // no trailing '/'

// matches requires that start with '../' (leaves child-relative requires alone)
var parentRequires = /(require\(['"])(\.\..*)(['"]\))/g;
var parentImports = /(import\s*\(?[\w\s{}]*['"])(\.\..*)(['"]\)?)/g;
// matches requires that start with './' (includes child-relative requires)
var allRequires = /(require\(['"])(\..*)(['"]\))/g;
var allImports = /(import\s*\(?[\w\s{}]*['"])(\..*)(['"]\)?)/g;

// add flag parsing
var opts = {};
[['all', '-a', '--all']].forEach(function (flags) {
  flags.slice(1).some(function (alias) {
    if (process.argv.slice(2).includes(alias)) {
      opts[flags[0]] = true;
    }
  });
});

async function rootify(pathname, filename) {
  // TODO not sure if this load order is exactly correct
  var loadable = ['.js', '.cjs', '.mjs', '.json'];
  if (!loadable.includes(path.extname(filename))) {
    //console.warn("# warn: skipping non-js file '%s'", filename);
    return;
  }

  var dirname = path.dirname(pathname);
  pathname = path.resolve(pathname);

  var requiresRe;
  var importsRe;
  if (opts.all) {
    requiresRe = allRequires;
    importsRe = allImports;
  } else {
    requiresRe = parentRequires;
    importsRe = parentImports;
  }

  var oldTxt = await fs.readFile(pathname, 'utf8');
  var changes = [];
  var txt = oldTxt.replace(requiresRe, replaceImports).replace(importsRe, replaceImports);

  function replaceImports(_, a, b, c) {
    //console.log(a, b, c);
    // a = 'require("' OR 'import("' OR 'import "'
    // b = '../../foo.js'
    // c = '")' OR ''

    // /User/me/project/lib/foo/bar + ../foo.js
    // becomes $/lib/foo/foo.js
    var pkgpath = '$' + path.resolve(dirname + '/', b).slice(pkglen);

    var result = a + pkgpath + c;
    changes.push([pkgpath, b]);
    return result;
  }

  if (oldTxt != txt) {
    console.info('\n# [', dirname, filename, ']');
    changes.forEach(function ([pkgpath, b]) {
      console.log('#', pkgpath, '<=', b);
    });
    await fs.writeFile(pathname, txt);
    console.info('git add', path.join(dirname, filename), ';');
  }
}

walk('.', async function (err, pathname, dirent) {
  if (['node_modules', '.git'].includes(dirent.name)) {
    return false;
  }

  if (!dirent.isFile()) {
    return;
  }

  return rootify(pathname, dirent.name).catch(function (e) {
    console.error(e);
  });
});

@root/walk:

async function walk(pathname, walkFunc, _dirent) {
  const fs = require('fs').promises;
  const path = require('path');
  const _pass = (err) => err;
  let dirent = _dirent;

  let err;

  // special case: walk the very first file or folder
  if (!dirent) {
    let filename = path.basename(path.resolve(pathname));
    dirent = await fs.lstat(pathname).catch(_pass);
    if (dirent instanceof Error) {
      err = dirent;
    } else {
      dirent.name = filename;
    }
  }

  // run the user-supplied function and either skip, bail, or continue
  err = await walkFunc(err, pathname, dirent).catch(_pass);
  if (false === err) {
    // walkFunc can return false to skip
    return;
  }
  if (err instanceof Error) {
    // if walkFunc throws, we throw
    throw err;
  }

  // "walk does not follow symbolic links"
  // (doing so could cause infinite loops)
  if (!dirent.isDirectory()) {
    return;
  }
  let result = await fs.readdir(pathname, { withFileTypes: true }).catch(_pass);
  if (result instanceof Error) {
    // notify on directory read error
    return walkFunc(result, pathname, dirent);
  }
  for (let entity of result) {
    await walk(path.join(pathname, entity.name), walkFunc, entity);
  }
}
janniks commented 3 years ago

Wow! Great idea πŸ’‘ would be happy to have that in basetag's bin. Would even use it frequently, IDEs and editors mostly prefer the ../s on auto-complete...

janniks commented 3 years ago

Script looks great, I'll test it out over the weekend πŸ‘πŸ»

janniks commented 3 years ago

Works pretty well so far β€” I can take over if you wish...

Leaving myself some notes for the script for later:

coolaj86 commented 3 years ago

For v1 I'd say make it simple, in repo, no dependencies - I could update this to use fs.readdir({withFileTypes: true}) rather than walk. If you need options, just process.argv.slice(2).includes("-y").

add mode for asking before each edit

Rather than this, I'd say add the reverse operation. Aside from that, git already handles the problem here.

coolaj86 commented 3 years ago

I updated the script above:

There's a LOT of ways to use imports, but most of them aren't useful in node (unless it's transpiled from some other language). I only support the basic usages:

import { x } as x from "whatever"
import x from "whatever"
await import("whatever").default
janniks commented 3 years ago

Thanks for the input, I like your proposal. πŸ‘πŸ»

Can you create a branch (on a fork) and commit your script? I will then create a feature branch and merge in your branch, so I can base off your work (and keep your contributions/commits) πŸ˜‰

In that feature branch I will switch basetag to a more script oriented approach. I'm thinking about a simple CLI that breaks down into a few scripts: