@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,630 lines (1,442 loc) • 70.5 kB
text/typescript
#! /usr/bin/env node
/**
* Copyright 2023 Adligo Inc / Scott Morgan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Buffer} from 'buffer';
import * as fs from 'fs';
import {PathLike, PathOrFileDescriptor, WriteFileOptions} from 'fs';
import {spawnSync, SpawnSyncOptions, SpawnSyncReturns} 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: string = "1.5.8";
// ########################### Interfaces ##################################
export interface I_CliCtx {
/**
* wrapps process.env[name]
* @param name
*/
envVar(name: string): string;
/**
* This is the absolute path of the current directory
* as a Unix path (although you might be running on Windows).
*/
getDir(): Path;
getFs(): I_Fs;
getKeys(): string[];
getShell(): string;
getShellOptionsFactory(): ShellOptionsFactory;
getValue(key: string): CliCtxArg;
getHome(): Path;
run(cmd: string, args: string[]): any;
runE(cmd: string, args: string[], options?: any, logLevel?: number): any;
isBash(): boolean;
isDebug(): boolean;
isDone(): boolean;
isInMap(key: string): boolean;
isWindows(): boolean;
logCmd(cmdWithArgs: string, spawnSyncReturns: any, options?: any, logLevel?: number): void;
getProc(): I_Proc;
/**
* Checks if the context is set to debug
* and if so prints the message, if you want to print regarless use print
* @param message
*/
out(message: string): void;
print(message: string): void;
setDir(): void;
}
/**
* This is a set of attributes that can be used on the Command Line as
* an Argument.
*/
export interface I_CliCtxFlag {
/**
* The full command that will be expected if the double dash (i.e. --help )is used
*/
cmd: string;
/**
* This is the description of what the command should do
*/
description?: string;
/**
* This indicates if the flag accepts arguments or is simply a flag,
* it defaults to true.
*/
flag?: boolean;
/**
* This is the single letter that can be concatinated together (i.e. f and r in rm -fr)
*/
letter?: string;
}
export interface I_CliCtxLog {
log(message: string): void;
setFileName(fileName: string): void;
}
/**
* @deprecated remove in 2030
*/
export interface I_DependencySLinkGroup {
group: string;
projects: I_DependencySLinkProject[];
}
/**
* @deprecated remove in 2030
*/
export interface I_DependencySLinkProject {
project: string;
modulePath: string;
}
/**
* @deprecated remove in 2030
*/
export interface I_DependencySrcSLink {
project: string;
srcPath: string;
destPath: string;
}
/**
* I_Fs provides the ability to stub out functions like readFileSync
* for testing
*/
export interface I_Fs {
/**
* Updates a file
*/
appendFileSync(
path: PathOrFileDescriptor,
data: string | Uint8Array,
options?: WriteFileOptions,
): void;
/**
* Copies a file
*/
copyFileSync(src: PathOrFileDescriptor, dest: PathOrFileDescriptor): void;
/**
* @param path the OS dependent absolute path
* @returns the string that represents the path that a Symlink is pointing at.
*/
getSymlinkTarget(path: string): string;
/**
* @param path the OS dependent relative path
* @param parentPath the absolute OS dependent path of the parent directory.
* @returns the string that represents the relative path that a Symlink is pointing at.
*/
getSymlinkTargetRelative(relativePath: string, parentPath: string, pathSeperator: string): string;
/**
* Identifies if this path is a Symlink or not
* @param path the OS dependent absolute path
* @returns True if the symlink exists, false otherwise.
*/
isSymlink(path: string): boolean;
readdirSync(
path: PathLike,
options?: {encoding: BufferEncoding | null, withFileTypes?: false | undefined, recursive?: boolean | undefined} | BufferEncoding | null,
): string[];
/**
* Reads a file
*/
readFileSync(path: PathOrFileDescriptor, optionsFlag?: string | undefined): string | undefined;
}
/**
* I_FsContext provides the ability to stub out functions like mkdir
* for testing
*/
export interface I_FsContext {
/**
* This determines if a path (folder or file) exists.
* @param path
*/
existsAbs(path: Path): boolean;
/**
* This determines if a path (folder or file) exists.
* @param relativePathParts
* @param inDir
*/
exists(fileOrDir: string, inDir: Path): boolean;
getFs(): I_Fs;
/**
* @param dir the absolute path of the Symlink
* @returns The string of the Symlink target, or a empty string '' if this can not be determined.
*/
getSymlinkTarget(dir: Path): Path;
/**
* @param dir the absolute path of the Symlink
* @returns True if the absolute path is a Symlink, false otherwise.
*/
isSymlink(dir: Path): boolean;
/**
* @param path the OS dependent relative path
* @param parentPath the absolute OS dependent path of the parent directory.
* @returns the string that represents the relative path that a Symlink is pointing at.
*/
getSymlinkTargetRelative(relativePath: Path, parentPath: Path): Path;
mkdir(dir: string, inDir: Path): void;
mkdirTree(dirs: Path, inDir: Path): Path;
read(path: Path, charset?: string): any;
readJson(path: Path): any;
rd(dir: string, inDir: Path): void;
rm(dir: string, inDir: Path): void;
/**
* create a new symbolic link
*/
slink(slinkName: string, toDir: Path, inDir: Path): void;
}
/**
* I_SlinkConsole provides the ability to stub out console.log
* for testing
*/
export interface I_SlinkConsole {
out(message: string): void;
}
/**
* I_Path represents a directory and or file path
*/
interface I_Path {
hasParent(): boolean;
isRelative(): boolean;
isRoot(): boolean;
isWindows(): boolean;
getParts(): string[];
getParent(): I_Path;
toString(): string;
toUnix(): string;
toWindows(): string;
child(path: string): I_Path;
}
/**
* I_Proc provides the ability to stub out process.env and process.env.SHELL
* for testing
*/
export interface I_Proc {
/**
* wrapps process.argv
*/
argv(): string[];
/**
* the Current Working Directory
* wrapps process.cws
*/
cwd(): string;
/**
* wrapps process.env
*/
env(): any;
/**
* wrapps process.env[name]
* @param name
*/
envVar(name: string): string;
/**
* wrapps console.error and is used to notify calling scripts that this script has NOT completed successfully
* by writing the test process.exit to the stderr. So that calling scripts can look for 'process.exit', as I was unable to find the actual
* error code number from the spawnSyncReturns object.
* @param name
*/
error(message: string);
/**
* This exits the current program, wrapps process.exit
*/
exit(code: number);
getPathSeperator(): string;
/**
* return true if it's windows otherwise false
*/
isWindows(): boolean;
/**
* wrapps process.env.SHELL
*/
shell(): string;
}
export interface I_SpawnSync {
spawnSync(command: string, args?: ReadonlyArray<string>, options?: SpawnSyncOptions): SpawnSyncReturns<string | Buffer<ArrayBufferLike>>;
}
// ################################ Stubs ###########################################
export class SlinkConsoleStub implements I_SlinkConsole {
out(message: string) {
outStatic(message)
}
}
export class SpawnSyncStub implements I_SpawnSync {
spawnSync(command: string, args?: ReadonlyArray<string>, options?: SpawnSyncOptions): SpawnSyncReturns<string | Buffer<ArrayBufferLike>> {
return spawnSync(command, args, options);
}
}
export class FsStub implements I_Fs {
appendFileSync(
path: fs.PathOrFileDescriptor,
data: string | Uint8Array,
options?: WriteFileOptions,
): void {
fs.appendFileSync(path, data, options);
}
/**
* @see {@link I_Fs#_copyFileSync}
*/
public copyFileSync(src: PathLike, dest: PathLike, mode?: number): void {
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: string): string {
return fs.realpathSync(path);
}
/**
* @see {@link I_Fs#getSymlinkTargetRelative}
*/
getSymlinkTargetRelative(relativePath: string, parentPath: string, pathSeperator: string): string {
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: string): boolean {
let stats = fs.lstatSync(path);
return stats.isSymbolicLink();
}
readdirSync(
path: PathLike,
options?: {encoding: BufferEncoding | null, withFileTypes?: false | undefined, recursive?: boolean | undefined} | BufferEncoding | null,
): string[] {
return fs.readdirSync(path, options);
}
readFileSync(path: PathOrFileDescriptor, optionsFlag?: string | undefined): string | undefined {
let options = null;
if (optionsFlag != null && optionsFlag != undefined) {
options ={ flag: optionsFlag }
}
return fs.readFileSync(path, options).toString();
}
}
/**
* @see {@link I_Proc}
*/
export class ProcStub implements I_Proc {
/**
* @see {@link I_Proc#argv}
*/
argv(): any {
return process.argv;
}
/**
* @see {@link I_Proc#cwd}
*/
cwd(): string {
return process.cwd();
}
/**
* @see {@link I_Proc#env}
*/
env(): any {
return process.env;
}
/**
* @see {@link I_Proc#error}
*/
error(message: string) {
console.error(message);
}
/**
* @see {@link I_Proc#envVar}
*/
envVar(name: string): string {
return process.env[name];
}
/**
* @see {@link I_Proc#exit}
*/
exit(code: number) {
process.exit(code);
}
getPathSeperator() {
if (this.isWindows()) {
return '\\';
} else {
return '/';
}
}
isWindows(): boolean {
return process.platform === "win32"
}
/**
* @see {@link I_Proc#shell}
*/
shell(): string {
return process.env.SHELL;
}
}
// ################################### Interface Implementation Constants #########################################
export const outStatic = (message) => console.log(message);
export const DEBUG: I_CliCtxFlag = { cmd: "debug", description: "Displays debugging information about htis program.", flag: true, letter: "d" }
export const DIR: I_CliCtxFlag = {
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: I_CliCtxFlag = { cmd: "log", description: "Writes a slink.log file in the run directory.", flag: false, letter: "l" }
export const HELP: I_CliCtxFlag = { cmd: "help", description: "Displays the Help Menu, prints this output.", flag: true, letter: "h" }
export const REMOVE: I_CliCtxFlag = { cmd: "remove", description: "Removes the symlinks.", flag: true, letter: "r" }
export const PUBLISH: I_CliCtxFlag = { cmd: "publish-local", description: "Publishes this compiled javascript to the current symlinked node_modules directory.", flag: true, letter: "p" }
export const VERSION: I_CliCtxFlag = { cmd: "version", description: "Displays the version.", flag: true, letter: "v" }
export const SHELL: I_CliCtxFlag = {
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: I_CliCtxFlag[] = [DEBUG, DIR, LOG, HELP, REMOVE, PUBLISH, SHELL, VERSION];
export enum LogLevel {
TRACE = 0,
DEBUG = 1,
INFO = 2,
WARN = 3,
ERROR = 4
}
// ################################## Classes ###########################################
export class ShellRunner {
console: I_SlinkConsole;
sSync?: I_SpawnSync = new SpawnSyncStub();
logLevel: number = LogLevel.INFO;
constructor(console: I_SlinkConsole, mockSpawnSync?: I_SpawnSync, logLevel?: number) {
this.console = console;
if (mockSpawnSync != undefined) {
this.sSync = mockSpawnSync;
}
if (logLevel != undefined) {
this.logLevel = logLevel;
}
}
public run(cmd: string, args: string[], options?: SpawnSyncOptions): SpawnSyncReturns<string | Buffer<ArrayBufferLike>> {
//stubbed for unit testing
let ssr: SpawnSyncReturns<string | Buffer<ArrayBufferLike>> = this.sSync.spawnSync(cmd, args, options);
return ssr;
}
}
export class ShellOptionsFactory {
/**
* @param ctx
* @param cwd the current working directory
*/
public getOptions(ctx: I_CliCtx, cwd: string, logLevel?: number): any {
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
*/
public getOptionsShell(ctx: I_CliCtx, logLevel?: number): any {
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
*/
public getShell(ctx: I_CliCtx, logLevel?: number): string {
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 {
private cmd: string;
private letter?: string;
private description?: string;
private flag: boolean;
constructor(flag: I_CliCtxFlag) {
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(): string { return this.cmd; }
getDescription(): string { return this.description; }
getFlag(): boolean { return this.flag; }
getLetter(): string { return this.letter; }
isFlag(): boolean { return this.flag; }
}
export class CliCtxArg {
private flag: CliCtxFlag;
private arg?: string;
constructor(flag: CliCtxFlag, arg?: string) {
this.flag = flag;
this.arg = arg;
}
getArg(): string { return this.arg }
getFlag(): CliCtxFlag { return this.flag }
}
export class CliCtxLog implements I_CliCtxLog {
private fileName?: string;
private messages: string[] = new Array();
private fsM: I_Fs;
constructor(fs?: I_Fs) {
if (fs == undefined) {
this.fsM = new FsStub();
} else {
this.fsM = fs;
}
}
log(message: string) {
if (this.fileName == undefined) {
this.messages = this.messages.concat(message);
} else {
this.fsM.appendFileSync(this.fileName, message);
}
}
setFileName(fileName: string) {
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 implements I_CliCtx {
public static WHEN_RUNNING_SLINK_ON_WINDOWS_YOU_MUST_USE_GITBASH_AS_ADMINISTRATOR = "When running slink on Windows you must use GitBash as Adminsitratior!";
private done: boolean = false;
/**
* This is the current working directory of your shell, if possible
* sometime you need to pass it in.
*/
private dir: Path;
private console: I_SlinkConsole;
private log: I_CliCtxLog;
private shellRun: ShellRunner;
private fsc: FsContext;
private fs: I_Fs;
private procIn: I_Proc;
/**
* this is the home directory where your application is installed,
* in the npm shared space of your computer
*/
private home: Path;
private map: Map<string, CliCtxArg> = new Map();
private sof: ShellOptionsFactory = 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: I_CliCtxFlag[], args?: string[], log?: I_CliCtxLog, console?: I_SlinkConsole, fs?: I_Fs, proc?: I_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: CliCtxFlag[] = new Array(flags.length);
let map2Cmds: Map<string, CliCtxFlag> = new Map();
let map2Letters: Map<string, CliCtxFlag> = 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: string[] = 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: CliCtxFlag = 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: CliCtxFlag = 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: I_CliCtxFlag = 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: string): string {
return this.procIn.envVar(key);
}
getDir(): Path {
return this.dir;
}
getFs(): I_Fs {
return this.fs;
}
getKeys(): string[] {
return Array.from(this.map.keys());
}
getShell(): string {
return this.procIn.shell();
}
getShellOptionsFactory(): ShellOptionsFactory {
return this.sof;
}
getValue(key: string): CliCtxArg {
return this.map.get(key);
}
getHome(): Path {
return this.home;
}
run(cmd: string, args: string[]): SpawnSyncReturns<string | Buffer<ArrayBufferLike>> {
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: string, args: string[], options?: SpawnSyncOptions): SpawnSyncReturns<string | Buffer<ArrayBufferLike>> {
let ssr = this.shellRun.run(cmd, args, options);
this.logCmd(cmd + ' ' + args, ssr, options);
return ssr;
}
isBash(): boolean {
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(): boolean {
return this.map.has(DEBUG.cmd);
}
isDone(): boolean {
return this.done;
}
isInMap(key: string): boolean {
return this.map.has(key);
}
isWindows(): boolean {
return this.procIn.isWindows();
}
/**
* Prints to the javascript console and also the log file when logging to a file
* @param message
*/
out(message: string) {
this.log.log(message);
this.console.out(message);
}
print(message: string) {
this.console.out(message);
}
getProc(): I_Proc {
return this.procIn;
}
setDir(): void {
let arg: CliCtxArg = this.map.get(DIR.cmd);
var dir: string = 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: string = 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: string, spawnSyncReturns: SpawnSyncReturns<string | Buffer<ArrayBufferLike>>, options?: SpawnSyncOptions): void {
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: Error | undefined = spawnSyncReturns.error.cause as Error | undefined;
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 as Error | undefined;
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);
}
}
private addCliCtxArg(flag: CliCtxFlag, cmd: string, i: number, args: string[]) {
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 implements I_FsContext {
private ctx: I_CliCtx;
private fs: I_Fs;
private funSsrExists = (ssr, ctx: I_CliCtx, path: 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: I_CliCtx, mockFs?: I_Fs) {
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: Path): boolean {
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: any = 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: any = 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: any = 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: string, inDir: Path): boolean {
if (this.ctx.isDebug()) {
this.ctx.out("in exists with path (toUnix) " + fileOrDir + " in " + inDir.toUnix());
}
return this.existsAbs(inDir.child(fileOrDir));
}
getFs(): I_Fs {
return this.fs;
}
isSymlink(dir: Path): boolean {
return this.fs.isSymlink(Paths.toOs(dir, this.ctx.isWindows()));
}
getSymlinkTarget(dir: Path): Path {
return Paths.newPath(this.fs.getSymlinkTarget(Paths.toOs(dir, this.ctx.isWindows())), true, this.ctx.isWindows());
}
getSymlinkTargetRelative(relativePath: Path, parentPath: Path): Path {
let rPath: string = Paths.toOs(relativePath, this.ctx.isWindows());
let aPath: string = 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: string, inDir: Path) {
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: Path, inDir: Path): Path {
var dirNames: string[] = dirs.getParts();
for (var i = 0; i < dirNames.length; i++) {
let dir: string = 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: Path, charset?: string): any {
try {
if (this.ctx.isWindows()) {
//don't use unix files for gitbash here
let p: string = path.toWindows();
if (this.ctx.isDebug()) {
this.ctx.out('reading ' + p);
}
return fs.readFileSync(p);
} else {
let p: string = 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: Path): any {
return JSON.parse(this.read(path));
}
rd(slinkName: string, inDir: Path): void {
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: string, inDir: Path) {
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: string, toDir: Path, inDir: Path) {
/*
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 {
private group: string;
private projects: DependencySLinkProject[];
private unixIn: string;
private unixTo: string;
constructor(info: I_DependencySLinkGroup, ctx: I_CliCtx) {
this.group = info.group;
this.projects = DependencySLinkProject.to(this.group, info.projects, ctx);
this.unixIn = 'node_modules/' + this.group;
}
getGroup(): string { return this.group; }
getProjects(): DependencySLinkProject[] { return this.projects; }
getUnixIn(): string { return this.unixIn; }
getUnixTo(): string { return this.unixTo; }
}
export class DependencySLinkProject {
static to(group: string, projects: I_DependencySLinkProject[], ctx: I_CliCtx): DependencySLinkProject[] {
let r = new Array(projects.length);
for (var i = 0; i < projects.length; i++) {
r[i] = new DependencySLinkProject(group, projects[i], ctx);
}
return r;
}
private project: string;
private modulePath: string;
private unixTo: Path;
constructor(group: string, info: I_DependencySLinkProject, ctx: I_CliCtx) {
this.project = info.project;
this.modulePath = info.modulePath;
this.unixTo = Paths.toPath('../../../' + info.project + '/src', true);
}
getProjectName(): string { return this.project; }
getModluePath(): string { return this.modulePath; }
getUnixTo(): Path { return this.unixTo; }
toString(): string {
return 'DependencySLinkProject [project=\'' + this.project + '\', modulePath=\'' +
this.modulePath + '\', unixTo=\'' + this.unixTo + '\']';
}
}
export class DependencySrcSLink {
private unixIn: string;
private unixTo: string;
private name: string;
constructor(slink: I_DependencySrcSLink, ctx: I_CliCtx) {
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(): string { return this.unixIn; }
getName(): string { return this.name; }
getUnixTo(): string { return this.unixTo; }
toString(): string {
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 {
public static UNABLE_TO_READ_SHARED_PACKAGE_JSON_AT = 'Unable to read a shared dependency projects package.json file at;\n\t';
public static THE_FOLLOWING_PACKAGE_JSON_IS_MISSING_THE_SUBSEQUENT_DEPENDENCIES = '\nThe following package.json is missing the subsequent dependencies;\n\t';
public static THE_FOLLOWING_PACKAGE_JSON_FILES_HAVE_MISMATCHED_VERSIONS = 'The following package.json files have mismatched versions for;\n\t';
private ctx: I_CliCtx;
private fsCtx: I_FsContext;
/**
* This is the parsed json from package.json where slink was run
*/
private projectJson: any;
private projectDeps: Map<string,string>;
/**
* This is the parsed json from the package.json that is above the target of the node_modules symlink
*/
private sharedJsonPath: Path;
private sharedJson: any;
private sharedDeps: Map<string,string>;
constructor(projectJson: any, ctx: I_CliCtx, fsCtx: I_FsContext, sharedJsonPath: Path) {
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.
*/
public checkForMismatch(): boolean {
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;
}
private addProjectDeps(pDeps, to: Map<string,string>) {
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 implements I_Path {
public static PARTS_MUST_HAVE_VALID_STRINGS = 'Parts must have valid strings! ';
public static PARTS_MUST_HAVE_NON_EMPTY_STRINGS = 'Parts must have non-empty strings! ';
public static RELATIVE_PARTS_MUST_HAVE_ENTRIES = 'Relative parts must have entries! ';
public static newPath(parent: Path, relative: Path) {
let parts = parent.getParts().concat(relative.getParts());
return new Path(parts, false, parent.isWindows());
}
private readonly _relative: boolean;
private readonly _parts: string[];
private readonly _windows: boolean;
constructor(parts: string[], relative?: boolean, windows?: boolean) {
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(): boolean {
if (this._parts.length >= 2) {
return true;
}
return false;
}
isRelative(): boolean {
return this._relative;
}
isRoot(): boolean {
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(): boolean {
return this._windows;
}
getParts(): string[] {
return this._parts.slice(0, this._parts.length);
}
getParent(): Path {
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(): string {
return 'Path [parts=[' + this._parts + '], relative=' + this._relative + ', windows=' + this._windows + ']'
}
toPathString(): string {
var r: string = '';
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(): string {
let b = '';
if (!this._relative) {
b = '/';
}
let pp: string[] = 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(): string {
let b = '';
let pp: string[] = 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: string) {
return new Path(this.getParts().concat(path), this._relative, this._windows);
}
private concat(start: string, sep: string): string {
var r: string = start;
if (this.isWindows()) {
for (var i = 1; i < this._parts.length; i++) {
if (this._parts.length - 1 == i) {
r = r.concat(this._part