@ts.adligo.org/slink
Version:
This is a simple Type Script Application that will create symbolic links from webpack config files with links created by webpack-link inside of your source folder, which improves speed of changing upstream Javascript or Typescript in a multiple project sy
1,424 lines (1,422 loc) • 64.9 kB
JavaScript
#! /usr/bin/env node
import * as fs from 'fs';
import { spawnSync } from 'child_process';
// ########################### Constants ################################
//The old code would read from the package.json file that this deploys with, now we need to sync manually oh well
// also update this in the package.json file
// package.json.version
export const VERSION_NBR = "1.5.8";
// ################################ Stubs ###########################################
export class SlinkConsoleStub {
out(message) {
outStatic(message);
}
}
export class SpawnSyncStub {
spawnSync(command, args, options) {
return spawnSync(command, args, options);
}
}
export class FsStub {
appendFileSync(path, data, options) {
fs.appendFileSync(path, data, options);
}
/**
* @see {@link I_Fs#_copyFileSync}
*/
copyFileSync(src, dest, mode) {
fs.copyFileSync(src, dest, mode);
}
/*
doesn't work hmm fell back to bash commands for this
existsSync(
path: fs.PathLike,
): boolean {
return fs.existsSync(path);
}
*/
/**
* @see {@link I_Fs#getSymlinkTarget}
*/
getSymlinkTarget(path) {
return fs.realpathSync(path);
}
/**
* @see {@link I_Fs#getSymlinkTargetRelative}
*/
getSymlinkTargetRelative(relativePath, parentPath, pathSeperator) {
let r = fs.realpathSync(parentPath + pathSeperator + relativePath);
if (r.length < parentPath.length) {
throw new Error('The following absolute path;\n\t' + r + '\n does not appear to be under\n\t' + parentPath);
}
return r.substring(parentPath.length + 1, r.length);
}
/**
* @see {@link I_Fs#isSymlink}
*/
isSymlink(path) {
let stats = fs.lstatSync(path);
return stats.isSymbolicLink();
}
readdirSync(path, options) {
return fs.readdirSync(path, options);
}
readFileSync(path, optionsFlag) {
let options = null;
if (optionsFlag != null && optionsFlag != undefined) {
options = { flag: optionsFlag };
}
return fs.readFileSync(path, options).toString();
}
}
/**
* @see {@link I_Proc}
*/
export class ProcStub {
/**
* @see {@link I_Proc#argv}
*/
argv() {
return process.argv;
}
/**
* @see {@link I_Proc#cwd}
*/
cwd() {
return process.cwd();
}
/**
* @see {@link I_Proc#env}
*/
env() {
return process.env;
}
/**
* @see {@link I_Proc#error}
*/
error(message) {
console.error(message);
}
/**
* @see {@link I_Proc#envVar}
*/
envVar(name) {
return process.env[name];
}
/**
* @see {@link I_Proc#exit}
*/
exit(code) {
process.exit(code);
}
getPathSeperator() {
if (this.isWindows()) {
return '\\';
}
else {
return '/';
}
}
isWindows() {
return process.platform === "win32";
}
/**
* @see {@link I_Proc#shell}
*/
shell() {
return process.env.SHELL;
}
}
// ################################### Interface Implementation Constants #########################################
export const outStatic = (message) => console.log(message);
export const DEBUG = { cmd: "debug", description: "Displays debugging information about htis program.", flag: true, letter: "d" };
export const DIR = {
cmd: "dir", description: "A parameter passing the working directory to run the application in, \n" +
"conventionally through --dir `pwd`. Note the Backticks.", flag: false
};
export const LOG = { cmd: "log", description: "Writes a slink.log file in the run directory.", flag: false, letter: "l" };
export const HELP = { cmd: "help", description: "Displays the Help Menu, prints this output.", flag: true, letter: "h" };
export const REMOVE = { cmd: "remove", description: "Removes the symlinks.", flag: true, letter: "r" };
export const PUBLISH = { cmd: "publish-local", description: "Publishes this compiled javascript to the current symlinked node_modules directory.", flag: true, letter: "p" };
export const VERSION = { cmd: "version", description: "Displays the version.", flag: true, letter: "v" };
export const SHELL = {
cmd: "shell", description: "Specifies the shell to use for subprocess execution (e.g., /bin/bash). \n" +
"Overrides the USHELL environment variable when present.", flag: false, letter: "s"
};
export const FLAGS = [DEBUG, DIR, LOG, HELP, REMOVE, PUBLISH, SHELL, VERSION];
export var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["TRACE"] = 0] = "TRACE";
LogLevel[LogLevel["DEBUG"] = 1] = "DEBUG";
LogLevel[LogLevel["INFO"] = 2] = "INFO";
LogLevel[LogLevel["WARN"] = 3] = "WARN";
LogLevel[LogLevel["ERROR"] = 4] = "ERROR";
})(LogLevel || (LogLevel = {}));
// ################################## Classes ###########################################
export class ShellRunner {
console;
sSync = new SpawnSyncStub();
logLevel = LogLevel.INFO;
constructor(console, mockSpawnSync, logLevel) {
this.console = console;
if (mockSpawnSync != undefined) {
this.sSync = mockSpawnSync;
}
if (logLevel != undefined) {
this.logLevel = logLevel;
}
}
run(cmd, args, options) {
//stubbed for unit testing
let ssr = this.sSync.spawnSync(cmd, args, options);
return ssr;
}
}
export class ShellOptionsFactory {
/**
* @param ctx
* @param cwd the current working directory
*/
getOptions(ctx, cwd, logLevel) {
var r = new Object();
r = { ...r, cwd: cwd };
r = { ...r, shell: this.getShell(ctx, logLevel) };
return r;
}
/**
* @param ctx
* @param cwd the current working directory
*/
getOptionsShell(ctx, logLevel) {
var r = new Object();
r = { ...r, shell: this.getShell(ctx, logLevel) };
return r;
}
/**
* Determines which shell to use for subprocess execution.
* Priority: 1) --shell command line parameter, 2) USHELL environment variable, 3) default shell
*/
getShell(ctx, logLevel) {
if (logLevel == undefined) {
logLevel = LogLevel.INFO;
}
// Check for --shell command line parameter first (highest priority)
if (ctx && ctx.isInMap(SHELL.cmd)) {
let shellArg = ctx.getValue(SHELL.cmd);
if (shellArg && shellArg.getArg()) {
if (logLevel <= LogLevel.DEBUG) {
ctx.out('Using shell from --shell parameter: ' + shellArg.getArg());
}
return shellArg.getArg();
}
}
// Check for USHELL environment variable (second priority)
let ushell = ctx.envVar('USHELL');
if (ushell) {
if (logLevel <= LogLevel.DEBUG) {
ctx.out('Using shell from USHELL environment variable: ' + ushell);
}
return ushell;
}
// Fall back to default shell (lowest priority)
let defaultShell = ctx.getShell();
if (logLevel <= LogLevel.DEBUG) {
ctx.out('Using default shell: ' + defaultShell);
}
return defaultShell;
}
}
export class CliCtxFlag {
cmd;
letter;
description;
flag;
constructor(flag) {
this.cmd = flag.cmd;
this.letter = flag.letter;
this.description = flag.description;
if (flag.flag == undefined) {
this.flag = true;
}
else {
this.flag = flag.flag;
}
}
getCmd() { return this.cmd; }
getDescription() { return this.description; }
getFlag() { return this.flag; }
getLetter() { return this.letter; }
isFlag() { return this.flag; }
}
export class CliCtxArg {
flag;
arg;
constructor(flag, arg) {
this.flag = flag;
this.arg = arg;
}
getArg() { return this.arg; }
getFlag() { return this.flag; }
}
export class CliCtxLog {
fileName;
messages = new Array();
fsM;
constructor(fs) {
if (fs == undefined) {
this.fsM = new FsStub();
}
else {
this.fsM = fs;
}
}
log(message) {
if (this.fileName == undefined) {
this.messages = this.messages.concat(message);
}
else {
this.fsM.appendFileSync(this.fileName, message);
}
}
setFileName(fileName) {
this.fileName = fileName;
if (this.messages.length > -1) {
this.messages.forEach((m) => {
this.fsM.appendFileSync(this.fileName, m);
});
}
}
}
/**
* This class acts as the main hub for test stubbing
*/
export class CliCtx {
static WHEN_RUNNING_SLINK_ON_WINDOWS_YOU_MUST_USE_GITBASH_AS_ADMINISTRATOR = "When running slink on Windows you must use GitBash as Adminsitratior!";
done = false;
/**
* This is the current working directory of your shell, if possible
* sometime you need to pass it in.
*/
dir;
console;
log;
shellRun;
fsc;
fs;
procIn;
/**
* this is the home directory where your application is installed,
* in the npm shared space of your computer
*/
home;
map = new Map();
sof = new ShellOptionsFactory();
/**
*
* @param flags
* @param args
* @param log The log to delegate to
* @param console The console interface to print messages directly
* @param fs
* @param proc a wrapper around 'proccess' to stub out things like 'process.env'
*/
constructor(flags, args, log, console, fs, proc) {
// do proc and args
if (proc != undefined) {
this.procIn = proc;
}
else {
this.procIn = new ProcStub();
}
if (args == undefined) {
args = this.procIn.argv();
}
// do additional constructor parameter assignments in constructor order
if (log == undefined) {
this.log = new CliCtxLog();
}
else {
this.log = log;
}
if (console != undefined) {
this.console = console;
}
else {
this.console = new SlinkConsoleStub();
}
if (fs != undefined) {
this.fsc = new FsContext(this, fs);
}
else {
this.fsc = new FsContext(this, new FsStub());
}
this.fs = this.fsc.getFs();
// When even --debug and --version aren't working
/*
console.warn('1.3.7 process.argv are ' + process.argv);
console.warn('args are ' + args);
if (args != undefined) {
console.warn('args are ' + JSON.stringify(args));
}
*/
this.shellRun = new ShellRunner(this.console, new SpawnSyncStub(), LogLevel.INFO);
let allFlags = new Array(flags.length);
let map2Cmds = new Map();
let map2Letters = new Map();
for (var i = 0; i < flags.length; i++) {
let f = new CliCtxFlag(flags[i]);
allFlags[i] = f;
if (map2Cmds.has(f.getCmd())) {
throw Error("The following command has been duplicated? " + f.getCmd());
}
map2Cmds.set(f.getCmd(), f);
if (f.getLetter() != undefined) {
if (map2Letters.has(f.getLetter())) {
throw Error("The following command has a duplicated letter? " + f.getCmd());
}
map2Letters.set(f.getLetter(), f);
}
}
this.home = Paths.toPath(args[1], false);
let homeParts = this.home.getParts();
this.home = Paths.toPath(new Path(homeParts.slice(0, homeParts.length - 2), false, this.isWindows()).toPathString(), false);
for (var i = 2; i < args.length; i++) {
let a = args[i];
//out('processing cli arg ' + a);
if (a.length < 2) {
let a = i - 1;
throw Error('Unable to parse command line arguments, issue at argument; ' + a);
}
else {
let dd = a.substring(0, 2);
if (dd == '--') {
let cmd = a.substring(2, a.length);
//out('cmd is ' + cmd);
let flag = map2Cmds.get(cmd);
if (flag == undefined) {
throw new Error('No flag found for command ' + cmd);
}
i = this.addCliCtxArg(flag, cmd, i, args);
}
else if (a.charAt(0) == '-') {
//process letters
for (var j = 1; j < a.length; j++) {
let l = a.charAt(j);
//out('processing letter ' + l);
let flag = map2Letters.get(l);
i = this.addCliCtxArg(flag, flag.getCmd(), i, args);
}
}
else {
throw Error('Unable to process command line argument ; ' + a);
}
}
}
if (this.isDebug()) {
this.out("Debug is enabled!");
this.out('Processing commands HELP and VERSION with ' + JSON.stringify(this.map));
let val = this.map.get(VERSION.cmd);
if (val == undefined) {
this.out('this.map.get(VERSION.cmd) is undefined');
}
else {
this.out('this.map.get(VERSION.cmd) is ' + JSON.stringify(val));
}
}
if (this.map.get(HELP.cmd) != undefined) {
//print the help menu;
this.console.out('This program understands the following commands;\n');
for (var i = 0; i < flags.length; i++) {
let flag = flags[i];
var m = '\t--' + flag.cmd;
if (flag.letter != undefined) {
m = m + ' / -' + flag.letter;
}
this.console.out(m);
if (flag.description != undefined) {
this.console.out('\t\t' + flag.description);
}
}
this.done = true;
}
else if (this.map.get(VERSION.cmd) != undefined) {
this.console.out(VERSION_NBR);
/*
console.log('Trying to read the version number from the slink install package.json at');
console.log('this.home = ' + this.home + " + package.json");
let homePkgJsonName: Path = new Path(this.home.getParts().concat('package.json'));
console.log('homePkgJsonName = ' + homePkgJsonName.toPathString());
//out('Got homePkgJson ' + homePkgJson + ' fs is ' + fs);
let jObj = JSON.parse(this.fs.readFileSync(homePkgJsonName.toPathString()));
//out('Got JSON ' + jObj);
this.print(jObj.version);
if (this.map.has(DEBUG.cmd)) {
this.print('from file: ' + homePkgJsonName.toPathString());
}
*/
this.done = true;
}
}
envVar(key) {
return this.procIn.envVar(key);
}
getDir() {
return this.dir;
}
getFs() {
return this.fs;
}
getKeys() {
return Array.from(this.map.keys());
}
getShell() {
return this.procIn.shell();
}
getShellOptionsFactory() {
return this.sof;
}
getValue(key) {
return this.map.get(key);
}
getHome() {
return this.home;
}
run(cmd, args) {
let options = this.sof.getOptions(this, Paths.toOs(this.dir, this.isWindows()), LogLevel.INFO);
let ssr = this.shellRun.run(cmd, args, options);
this.logCmd(cmd + ' ' + args, ssr, options);
return ssr;
}
runE(cmd, args, options) {
let ssr = this.shellRun.run(cmd, args, options);
this.logCmd(cmd + ' ' + args, ssr, options);
return ssr;
}
isBash() {
let shell = this.procIn.shell();
if (this.map.has(DEBUG.cmd)) {
this.out('process.env.SHELL is ' + shell);
}
if (shell != undefined) {
if (shell.toLocaleLowerCase().includes('bash')) {
return true;
}
}
return false;
}
isDebug() {
return this.map.has(DEBUG.cmd);
}
isDone() {
return this.done;
}
isInMap(key) {
return this.map.has(key);
}
isWindows() {
return this.procIn.isWindows();
}
/**
* Prints to the javascript console and also the log file when logging to a file
* @param message
*/
out(message) {
this.log.log(message);
this.console.out(message);
}
print(message) {
this.console.out(message);
}
getProc() {
return this.procIn;
}
setDir() {
let arg = this.map.get(DIR.cmd);
var dir = process.cwd();
if (arg == undefined) {
if (this.map.has('trace')) {
this.out('process.env is ' + JSON.stringify(process.env));
}
if (process.env.PWD != undefined) {
var dir = process.env.PWD;
if (this.map.has('debug')) {
this.out('process.env.PWD is ' + dir);
}
}
if (dir == undefined) {
throw Error('Unable to determine the current working directory, please specify it with --dir <someFolder/>');
}
}
else {
dir = this.map.get(DIR.cmd).getArg();
}
if (this.map.has('debug')) {
this.out('before toOsPath CliCtx.dir is ' + dir);
}
this.dir = Paths.toPath(dir, false);
if (this.isDebug()) {
this.out('after toOsPath CliCtx.dir is ' + this.dir.toString());
}
if (this.map.has(LOG.cmd)) {
let logFileName = new Path(this.dir.getParts().concat('slink.log'), false, this.isWindows()).toPathString();
this.out('writing to logfile ' + logFileName);
this.log.setFileName(logFileName);
}
}
logCmd(cmdWithArgs, spawnSyncReturns, options) {
if (this.isDebug()) {
this.out('ran ' + cmdWithArgs + ' in \n\t' + options.cwd);
}
if (options != undefined) {
if (options.cwd != undefined) {
if (this.isDebug()) {
this.out('\tin ' + options.cwd);
}
}
else {
if (this.isDebug()) {
this.out('\tin ' + this.getDir());
}
}
}
else {
if (this.isDebug()) {
this.out('\tin ' + this.getDir());
}
}
if (this.isDebug()) {
this.out('\tand the spawnSyncReturns had;');
}
if (spawnSyncReturns.error != undefined) {
if (this.isDebug()) {
this.out('Error: ' + spawnSyncReturns.error);
this.out('Error Message: ' + spawnSyncReturns.error.message);
this.out('Error Stack: ' + spawnSyncReturns.error.stack);
var cause = spawnSyncReturns.error.cause;
var counter = 1;
while (cause != undefined) {
this.out('\n\nError Cause ' + counter + ': ' + cause.message);
this.out('Error Cause ' + counter + ' Stack: ' + cause.stack);
cause = cause.cause;
counter++;
}
}
}
if (spawnSyncReturns.stderr != undefined) {
if (this.isDebug()) {
this.out('\tStderr: ' + spawnSyncReturns.stderr);
}
}
if (spawnSyncReturns.stdout != undefined) {
if (this.isDebug()) {
if (spawnSyncReturns.stdout.length >= 100) {
if (this.isDebug()) {
this.out('\tStdout: ' + spawnSyncReturns.stdout);
}
else {
this.out('\tStdout: ' + spawnSyncReturns.stdout.slice(0, 100) + ' ... \n');
}
}
else {
this.out('\tStdout: ' + spawnSyncReturns.stdout);
}
}
}
if (spawnSyncReturns.status != 0) {
throw new Error('The command ' + cmdWithArgs + ' in dir \n\t' + options.cwd + ' \n\t Failed with exit code: ' + spawnSyncReturns.status);
}
}
addCliCtxArg(flag, cmd, i, args) {
if (flag.isFlag()) {
this.map.set(cmd, new CliCtxArg(flag));
}
else if (i + 1 < args.length) {
let arg = args[i + 1];
i++;
this.map.set(cmd, new CliCtxArg(flag, arg));
}
else {
throw Error('The following command line argument expects an additional argument; ' + cmd);
}
return i;
}
}
export class FsContext {
ctx;
fs;
funSsrExists = (ssr, ctx, path) => {
var t = false;
if (ssr.output != undefined) {
let outStr = ssr.output.toString();
let idx = outStr.indexOf("YES-EXISTS");
var t = idx != -1;
if (ctx.isDebug()) {
ctx.out("outStr is '" + outStr + "'" + " idx is " + idx + " t is " + t);
}
}
if (ctx.isDebug()) {
if (t) {
ctx.out("The following path exists; " + path.toPathString());
}
else {
ctx.out("The following path does NOT exist; " + path.toPathString());
}
}
return t;
};
constructor(cliCtx, mockFs) {
this.ctx = cliCtx;
if (mockFs != undefined) {
this.fs = mockFs;
}
else {
this.fs = new FsStub();
}
}
/**
* This determines if a path (folder or file) exists.
* @param path
*/
existsAbs(path) {
if (this.ctx.isDebug()) {
this.ctx.out("in existsAbs with path (toUnix) " + path.toUnix());
// hmm circular structure ;
//ctx.out("in existsAbs ctx " + JSON.stringify(ctx));
}
let sof = this.ctx.getShellOptionsFactory();
if (this.ctx.isWindows()) {
//existsSync s broken! for symblic links at least, this is clugy hack
if (this.ctx.isBash()) {
/*
hmm didn't work on Windows
if (ctx.getFs().existsSync(Paths.toUnix(path))) {
if (ctx.isDebug()) {
ctx.out("The following path exists; " + path.toPathString());
}
return true;
}
if (ctx.isDebug()) {
ctx.out("The following path does NOT exist; " + path.toPathString());
}
return false;
*/
/*
let ssr: any = this.ctx.run('ls', [Paths.toUnix(path)], undefined, LogLevel.TRACE);
var t = this.funSsrExists(ssr, this.ctx, path);
*/
//let cmd = 'echo `[[ -d "test_data" || -f "test_data" ]] && echo "YES" || echo "NO"`';
let cmd = 'echo `[[ -d "' + path.toUnix() + '" || -f "' + path.toUnix() +
'" ]] && echo "YES-EXISTS" || echo "NO-NOT-EXISTS"`';
let options = sof.getOptionsShell(this.ctx);
let ssr = this.ctx.runE(cmd, [], options);
return this.funSsrExists(ssr, this.ctx, path);
}
else {
let options = sof.getOptions(this.ctx, Paths.toOs(path, this.ctx.isWindows()));
let ssr = this.ctx.runE('dir', [], options);
return this.funSsrExists(ssr, this.ctx, path);
}
}
else {
let options = sof.getOptionsShell(this.ctx);
let cmd = 'echo `[[ -d "' + path.toUnix() + '" || -f "' + path.toUnix() +
'" ]] && echo "YES-EXISTS" || echo "NO-NOT-EXISTS"`';
if (this.ctx.isDebug()) {
this.ctx.out('Executing cmd ' + cmd);
}
let ssr = this.ctx.runE(cmd, [], options);
return this.funSsrExists(ssr, this.ctx, path);
}
}
/**
* This determines if a path (folder or file) exists.
* @param relativePathParts
* @param inDir
*/
exists(fileOrDir, inDir) {
if (this.ctx.isDebug()) {
this.ctx.out("in exists with path (toUnix) " + fileOrDir + " in " + inDir.toUnix());
}
return this.existsAbs(inDir.child(fileOrDir));
}
getFs() {
return this.fs;
}
isSymlink(dir) {
return this.fs.isSymlink(Paths.toOs(dir, this.ctx.isWindows()));
}
getSymlinkTarget(dir) {
return Paths.newPath(this.fs.getSymlinkTarget(Paths.toOs(dir, this.ctx.isWindows())), true, this.ctx.isWindows());
}
getSymlinkTargetRelative(relativePath, parentPath) {
let rPath = Paths.toOs(relativePath, this.ctx.isWindows());
let aPath = Paths.toOs(parentPath, this.ctx.isWindows());
if (this.ctx.isDebug()) {
this.ctx.out("in getSymlinkTargetRelative rPath '" + rPath + "' \n\t aPath is '" + aPath + "'");
}
var pathSeperator = '/';
if (this.ctx.isWindows()) {
pathSeperator = '\\';
}
let r = this.fs.getSymlinkTargetRelative(rPath, aPath, pathSeperator);
return Paths.newPath(r, true, this.ctx.isWindows());
}
mkdir(dir, inDir) {
if (this.ctx.isWindows()) {
this.ctx.runE('mkdir', [dir], this.ctx.getShellOptionsFactory().getOptions(this.ctx, Paths.toOs(inDir, this.ctx.isWindows())));
}
else {
this.ctx.runE('mkdir', [dir], this.ctx.getShellOptionsFactory().getOptions(this.ctx, inDir.toUnix()));
}
}
mkdirTree(dirs, inDir) {
var dirNames = dirs.getParts();
for (var i = 0; i < dirNames.length; i++) {
let dir = dirNames[i];
if (!this.exists(dir, inDir)) {
this.mkdir(dir, inDir);
}
inDir = new Path(inDir.getParts().concat(dir), false, inDir.isWindows());
}
return inDir;
}
read(path, charset) {
try {
if (this.ctx.isWindows()) {
//don't use unix files for gitbash here
let p = path.toWindows();
if (this.ctx.isDebug()) {
this.ctx.out('reading ' + p);
}
return fs.readFileSync(p);
}
else {
let p = path.toUnix();
if (this.ctx.isDebug()) {
this.ctx.out('reading ' + p);
}
return fs.readFileSync(p);
}
}
catch (e) {
this.ctx.print('Error reading file ' + path.toString());
this.ctx.print(e.message);
throw e;
}
}
readJson(path) {
return JSON.parse(this.read(path));
}
rd(slinkName, inDir) {
let sof = this.ctx.getShellOptionsFactory();
let inDirP = inDir.toWindows();
var options = sof.getOptions(this.ctx, inDirP);
//var options ={ cwd: Paths.toUnix(inDir), shell: process.env.SHELL}
if (this.ctx.isDebug()) {
this.ctx.out("Using shell " + options.shell + " in Windows dir " + options.cwd);
this.ctx.out("Removing link named " + slinkName);
}
let result = this.ctx.runE('echo \'rd .\\' + slinkName + '\' | cmd', [], options, LogLevel.TRACE);
if (this.ctx.isDebug()) {
this.ctx.out("rd result stdout is " + result.stdout);
this.ctx.out("rd result stderr is " + result.stderr);
}
}
rm(dir, inDir) {
if (this.ctx.isDebug()) {
this.ctx.out("in rm (toUnix) " + dir + " in " + inDir.toUnix());
}
if (this.exists(dir, inDir)) {
if (this.ctx.isWindows()) {
//existsSync s broken!
this.ctx.runE('rm', ['-fr', dir], { cwd: inDir.toWindows() });
}
else {
this.ctx.runE('rm', ['-fr', dir], { cwd: inDir.toUnix() });
}
}
else {
if (this.ctx.isDebug()) {
this.ctx.out(dir + " doesn't exist' in " + inDir.toUnix());
}
}
}
/**
* create a new symbolic link
*/
slink(slinkName, toDir, inDir) {
/*
var sp = "Creating symlink from node_modules in;\n\t " + Paths.toOs(this.ctx.getDir(), this.ctx.isWindows());
sp += "\n\tto \n\t" + Paths.toOs(parentProjectWithNodeModulesPath, this.ctx.isWindows());
this.ctx.print(sp);
*/
let sof = this.ctx.getShellOptionsFactory();
if (this.ctx.isWindows()) {
let toDirP = toDir.toWindows();
let inDirP = inDir.toWindows();
if (this.ctx.isDebug()) {
this.ctx.out("Linking to " + toDir);
}
var options = sof.getOptions(this.ctx, inDirP);
//var options ={ cwd: Paths.toUnix(inDir), shell: process.env.SHELL}
if (this.ctx.isDebug()) {
this.ctx.out("In FsContext.slinkUsing shell " + options.shell + " in Windows dir " + options.cwd);
this.ctx.out("Creating link named " + slinkName + " to \n\t " + toDirP);
this.ctx.out("All options are " + JSON.stringify(options));
let pwdResult = this.ctx.runE('pwd', [], options, LogLevel.TRACE);
this.ctx.out("pwdResult is \n\t" + pwdResult.stdout);
}
let result = this.ctx.runE('echo \'mklink /J ' + slinkName + ' ' + toDirP + '\' | cmd', [], options, LogLevel.TRACE);
if (this.ctx.isDebug()) {
this.ctx.out("mklink result.stdout is \n\t" + result.stdout);
this.ctx.out("mklink result.stderr is \n\t" + result.stderr);
}
//note you must be adminsitrator or have heightened privlages to do this on Windows, so double check if
// it got done
let success = this.existsAbs(inDir.child(slinkName));
if (success) {
//do nothing
}
else {
throw Error('Unable to create the following link, are you running slink as Administrator or with heightened privleges?' +
new Path(toDir.getParts().concat(slinkName), false, true).toString());
}
}
else {
let inDirP = inDir.toUnix();
let toDirP = toDir.toUnix();
let options = sof.getOptions(this.ctx, inDirP);
this.ctx.runE('ln', ['-s', '-T', toDirP, slinkName], options);
}
}
}
export class DependencySLinkGroup {
group;
projects;
unixIn;
unixTo;
constructor(info, ctx) {
this.group = info.group;
this.projects = DependencySLinkProject.to(this.group, info.projects, ctx);
this.unixIn = 'node_modules/' + this.group;
}
getGroup() { return this.group; }
getProjects() { return this.projects; }
getUnixIn() { return this.unixIn; }
getUnixTo() { return this.unixTo; }
}
export class DependencySLinkProject {
static to(group, projects, ctx) {
let r = new Array(projects.length);
for (var i = 0; i < projects.length; i++) {
r[i] = new DependencySLinkProject(group, projects[i], ctx);
}
return r;
}
project;
modulePath;
unixTo;
constructor(group, info, ctx) {
this.project = info.project;
this.modulePath = info.modulePath;
this.unixTo = Paths.toPath('../../../' + info.project + '/src', true);
}
getProjectName() { return this.project; }
getModluePath() { return this.modulePath; }
getUnixTo() { return this.unixTo; }
toString() {
return 'DependencySLinkProject [project=\'' + this.project + '\', modulePath=\'' +
this.modulePath + '\', unixTo=\'' + this.unixTo + '\']';
}
}
export class DependencySrcSLink {
unixIn;
unixTo;
name;
constructor(slink, ctx) {
this.name = slink.project + '@slink';
if (slink.destPath == undefined) {
this.unixIn = 'src';
}
else {
this.unixIn = slink.destPath;
}
if (slink.srcPath == undefined) {
this.unixTo = '../../' + slink.project + '/src';
}
else {
this.unixTo = '../../' + slink.project + slink.srcPath;
}
}
getUnixIn() { return this.unixIn; }
getName() { return this.name; }
getUnixTo() { return this.unixTo; }
toString() {
return 'DependencySLink [name=\'' + this.name + '\', unixIn=\'' + this.unixIn +
'\', unixTo=\'' + this.unixTo + '\']';
}
}
/**
* This class compares two package.json files to ensure that the packages and versions of those
* packages are identical, since we are symlinking the projects together the packages and versions
* required should match.
*/
export class PackageJsonComparator {
static UNABLE_TO_READ_SHARED_PACKAGE_JSON_AT = 'Unable to read a shared dependency projects package.json file at;\n\t';
static THE_FOLLOWING_PACKAGE_JSON_IS_MISSING_THE_SUBSEQUENT_DEPENDENCIES = '\nThe following package.json is missing the subsequent dependencies;\n\t';
static THE_FOLLOWING_PACKAGE_JSON_FILES_HAVE_MISMATCHED_VERSIONS = 'The following package.json files have mismatched versions for;\n\t';
ctx;
fsCtx;
/**
* This is the parsed json from package.json where slink was run
*/
projectJson;
projectDeps;
/**
* This is the parsed json from the package.json that is above the target of the node_modules symlink
*/
sharedJsonPath;
sharedJson;
sharedDeps;
constructor(projectJson, ctx, fsCtx, sharedJsonPath) {
this.projectJson = projectJson;
this.ctx = ctx;
if (this.ctx.isDebug()) {
this.ctx.out('in PackageJsonComparator constructor \n' +
JSON.stringify(projectJson.dependencies) + '\n' + JSON.stringify(projectJson.devDependencies));
}
this.fsCtx = fsCtx;
this.sharedJsonPath = sharedJsonPath;
if (fsCtx.existsAbs(sharedJsonPath)) {
this.sharedJson = fsCtx.readJson(sharedJsonPath);
}
else {
throw new Error(PackageJsonComparator.UNABLE_TO_READ_SHARED_PACKAGE_JSON_AT + sharedJsonPath.toPathString());
}
// Get all project dependencies
const pd = {
...(this.projectJson.dependencies || {}),
...(this.projectJson.devDependencies || {})
};
this.projectDeps = new Map(Object.entries(pd));
if (this.projectDeps.size == 0) {
this.sharedDeps = new Map();
return;
}
// Get all shared dependencies
const sd = {
...(this.sharedJson.dependencies || {}),
...(this.sharedJson.devDependencies || {})
};
this.sharedDeps = new Map(Object.entries(sd));
}
/**
* This checks for a mismatch versions or existence of dependencies in the sharedDeps with the projectDeps
* @returns false if there is no mismatch, true if a mismatch has occurred.
*/
checkForMismatch() {
if (this.ctx.isDebug()) {
this.ctx.out('in PackageJsonComparator checkForMismatch');
}
if (this.projectDeps.size == 0) {
return false;
}
var missing = PackageJsonComparator.THE_FOLLOWING_PACKAGE_JSON_IS_MISSING_THE_SUBSEQUENT_DEPENDENCIES;
missing += this.sharedJsonPath.toPathString() + '\n\t';
var mismatched = PackageJsonComparator.THE_FOLLOWING_PACKAGE_JSON_FILES_HAVE_MISMATCHED_VERSIONS;
mismatched += this.sharedJsonPath.toPathString() + '\n\t';
mismatched += new Path(this.ctx.getDir().getParts(), false, this.ctx.isWindows()).child('package.json').toPathString() + '\n\t';
var hasMissing = false;
var hasMismatch = false;
for (const [key, value] of this.projectDeps) {
if (this.ctx.isDebug()) {
this.ctx.out('Checking project dependency ' + key + ',' + value);
}
if (this.sharedDeps.has(key)) {
let sVal = this.sharedDeps.get(key);
if (value != sVal) {
hasMismatch = true;
mismatched += key + ' ' + value + ' vs shared ' + sVal + '\n\t';
}
}
else {
hasMissing = true;
missing += key + ' ' + value + '\n\t';
}
}
if (hasMismatch || hasMissing) {
if (hasMissing) {
this.ctx.out(missing);
}
if (hasMismatch) {
this.ctx.out(mismatched);
}
return true;
}
return false;
}
addProjectDeps(pDeps, to) {
if (pDeps != undefined) {
if (this.ctx.isDebug()) {
this.ctx.out('Object.keys(pDeps).length is ' + Object.keys(pDeps).length);
}
Object.keys(pDeps).forEach(key => {
let value = pDeps[key];
if (this.ctx.isDebug()) {
this.ctx.out('adding ' + key + ' ' + value);
}
to.set(key, value);
});
}
else {
if (this.ctx.isDebug()) {
this.ctx.out('pDeps is undefined ');
}
}
}
}
export class Path {
static PARTS_MUST_HAVE_VALID_STRINGS = 'Parts must have valid strings! ';
static PARTS_MUST_HAVE_NON_EMPTY_STRINGS = 'Parts must have non-empty strings! ';
static RELATIVE_PARTS_MUST_HAVE_ENTRIES = 'Relative parts must have entries! ';
static newPath(parent, relative) {
let parts = parent.getParts().concat(relative.getParts());
return new Path(parts, false, parent.isWindows());
}
_relative;
_parts;
_windows;
constructor(parts, relative, windows) {
if (relative == undefined) {
this._relative = false;
}
else {
this._relative = relative;
}
this._parts = parts;
for (var i = 0; i < parts.length; i++) {
if (parts[i] == undefined) {
throw Error(Path.PARTS_MUST_HAVE_VALID_STRINGS + parts);
}
else if (parts[i].trim() == '') {
throw Error(Path.PARTS_MUST_HAVE_NON_EMPTY_STRINGS + parts);
}
}
if (this._parts.length == 0 && this._relative != false) {
throw Error(Path.RELATIVE_PARTS_MUST_HAVE_ENTRIES + parts);
}
if (windows == undefined) {
this._windows = false;
}
else {
this._windows = windows;
}
}
hasParent() {
if (this._parts.length >= 2) {
return true;
}
return false;
}
isRelative() {
return this._relative;
}
isRoot() {
if (this._relative) {
return false;
}
if (this._windows) {
if (this._parts.length == 1) {
return true;
}
}
else {
if (this._parts.length == 0) {
return true;
}
}
return false;
}
isWindows() {
return this._windows;
}
getParts() {
return this._parts.slice(0, this._parts.length);
}
getParent() {
if (this._parts.length >= 2) {
return new Path(this._parts.slice(0, this._parts.length - 1), this._relative, this._windows);
}
else {
throw new Error("The path " + this.toPathString() + " has no parents! ");
}
}
toString() {
return 'Path [parts=[' + this._parts + '], relative=' + this._relative + ', windows=' + this._windows + ']';
}
toPathString() {
var r = '';
if (this._windows) {
if (this._relative) {
r = r.concat(this._parts[0] + '\\');
return this.concat(r, '\\');
}
else {
r = r.concat(this._parts[0] + ':\\');
return this.concat(r, '\\');
}
}
else {
if (this._relative) {
return this.concat(r, '/');
}
else {
r = r.concat('/');
return this.concat(r, '/');
}
}
}
toUnix() {
let b = '';
if (!this._relative) {
b = '/';
}
let pp = this._parts;
for (var i = 0; i < pp.length; i++) {
let p = pp[i];
if (i == pp.length - 1) {
b = b.concat(p);
}
else {
b = b.concat(p).concat('/');
}
}
return b;
}
toWindows() {
let b = '';
let pp = this._parts;
for (var i = 0; i < pp.length; i++) {
if (i == 0) {
if (pp[0].length == 1) {
b = pp[0].toUpperCase() + ':\\';
}
else {
b = pp[0].concat('\\');
}
}
else if (i == pp.length - 1) {
b = b.concat(pp[i]);
}
else {
b = b.concat(pp[i]).concat('\\');
}
}
return b;
}
child(path) {
return new Path(this.getParts().concat(path), this._relative, this._windows);
}
concat(start, sep) {
var r = start;
if (this.isWindows()) {
for (var i = 1; i < this._parts.length; i++) {
if (this._parts.length - 1 == i) {
r = r.concat(this._parts[i]);
}
else {
r = r.concat(this._parts[i]).concat(sep);
}
}
return r;
}
else {
for (var i = 0; i < this._parts.length; i++) {
if (this._parts.length - 1 == i) {
r = r.concat(this._parts[i]);
}
else {
r = r.concat(this._parts[i]).concat(sep);
}
}
return r;
}
}
}
export class Paths {
static find(parts, relativePath) {
var dd = 0;
let rpp = relativePath.getParts();
for (var i = 0; i < rpp.length; i++) {
if (rpp[i] == '..') {
dd++;
}
}
let pp = parts.getParts();
//console.log('In find with dd ' + dd + '\n\tpath: ' + parts + '\n\trelativepath: ' + relativePath);
let root = pp.slice(0, pp.length - dd);
//console.log('Root is: ' + root);
var r = root;
for (var i = 0; i < rpp.length; i++) {
if (rpp[i] != '..') {
r = r.concat(rpp[i]);
}
}
//console.log('New relative path is\n\t' + r);
return new Path(r, false);
}
static findPath(path, relativePath) {
return this.find(this.toPath(path, false), relativePath);
}
static toOs(parts, isWindows) {
if (isWindows) {
return parts.toWindows();
}
else {
return parts.toUnix();
}
}
static newPath(path, relative, windows) {
return new Path(Paths.toPath(path, relative).getParts(), relative, windows);
}
/**
* @param a path, which could be
* a windows path (i.e. C:\User\scott ),
* a unix path (/home/scott)
* or a gitbash path (i.e. C:/Users/scott)
* Because of this spaces are NOT allowed.
*/
static toPath(path, relative) {
let r = new Array();
let b = '';
var j = 0;
var i = 0;
var winPath = false;
if (path.length >= 1) {
if (path.charAt(1) == ':') {
//it's a windows path
r[0] = path.charAt(0);
j++;
i = 3;
winPath = true;
}
}
for (; i < path.length; i++) {
let c = path[i];
if (c == '\\') {
if (b.length != 0) {
r[j] = b;
b = '';
j++;
}
}
else if (c == '/') {
if (b.length != 0) {
r[j] = b;
b = '';
j++;
}
}
else if (c == ' ') {
throw Error('Spaces are NOT allowed in paths, due to portability issues. The following path is invaid;\n\t' + path);
}
else {
b = b.concat(c);
}
}
r[j] = b;
if (relative == undefined) {
return new Path(r, false);
}
else if (relative) {
return new Path(r, relative);
}
else {
return new Path(r, relative);
}
}
}
export class SLinkRunner {
ctx;
fsCtx;
constructor(ctx, fsCtx) {
this.ctx = ctx;
if (fsCtx != undefined) {
this.fsCtx = fsCtx;
}
else {
this.fsCtx = new FsContext(ctx);
}
}
/**
*
* @param envVars
* @returns true if was processed, false if wasn't
*/
handleSharedNodeModulesViaEnvVar(envVars, projectJson) {
this.ctx.print("Processing sharedNodeModuleProjectSLinkEnvVar: " + JSON.stringify(envVars));
for (const envVar of envVars) {
const envValue = this.ctx.getProc().envVar(envVar);
if (envValue) {
if (this.ctx.isDebug()) {
this.ctx.out(`Found environment variable ${envVar} with value ${envValue}`);
}
let envValPath = Paths.newPath(envValue, false, this.ctx.isWindows());
if (this.fsCtx.existsAbs(envValPath)) {
if (this.fsCtx.exists('node_modules', this.ctx.getDir())) {
// Remove existing node_modules if it exists
this.removeNodeModules();
}
let comp = new PackageJsonComparator(projectJson, this.ctx, this.fsCtx, envValPath.getParent().child('package.json'));
if (comp.checkForMismatch()) {
this.ctx.getProc().error('Unable to complete successfully, process.exit(' + 1870 + ')');
this.ctx.getProc().exit(1870);
}
// Create symlink to the environment variable path
let targetPath = envValPath;
this.ctx.print(`Creating symlink from node_modules to ${Paths.toOs(targetPath, this.ctx.isWindows())}`);
this.fsCtx.slink('node_modules', targetPath, this.ctx.getDir());
return true; // Use the first valid environment variable
}
}
else {
this.ctx.print(`Environment variable ${envVar} NOT found or empty`);
}
}
return false;
}
handleSharedNodeModulesViaProjectLinks(projectNames, projectJson) {
this.ctx.print("Processing sharedNodeModuleProjectSLinks: " + JSON.stringify(projectNames));
let projectDir = this.ctx.getDir();
// Start from the current directory and traverse up the tree
let transRoot = projectDir.getParent();
let transParts = transRoot.getParts();
var counter = transParts.length;
var parentProjectWithNodeModulesPath;
while (counter >= 0) { // Stop at root (length 1 for drive/root)
// Calculate parent directory by removing the last part
const parentDirParts = transParts.slice(0, counter);
const parentDir = new Path(parentDirParts, false, this.ctx.isWindows());
if (this.ctx.isDebug()) {
this.ctx.out(`Checking parent directory: ${Paths.toOs(parentDir, this.ctx.isWindows())}`);
}
for (const projectName of projectNames) {
// Construct the full path to the potential project directory
const projectPathParts = parentDirParts.concat(projectName);
const projectPath = new Path(projectPathParts, false, this.ctx.isWindows());
if (this.ctx.isDebug()) {
this.ctx.out(`Checking for project ${projectName} at ${Paths.toOs(projectPath, this.ctx.isWindows())}`);
}
// Check if the project directory exists
if (this.fsCtx.existsAbs(projectPath)) {
// Check for node_modules in the project
const nodeModulesPathParts = projectPathParts.concat('node_modules');
const nodeModulesPath = new Path(nodeModulesPathParts, false, this.ctx.isWindows());
let nodeModulesExists = this.fsCtx.existsAbs(nodeModulesPath);
if (nodeModulesExists) {
parentProjectWithNodeModulesPath = nodeModulesPath;
break;
}
else {
// Attempt to run npm install if node_modules doesn't exist
if (this.ctx.isDebug()) {