UNPKG

@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
#! /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()) {