lincd-cli
Version:
Command line tools for the lincd.js library
1,128 lines (1,125 loc) ⢠112 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
import chalk from 'chalk';
import { exec } from 'child_process';
import depcheck from 'depcheck';
import { getEnvFile } from 'env-cmd/dist/get-env-vars.js';
import fs from 'fs-extra';
import path, { dirname } from 'path';
import { createInterface } from 'readline';
import { debugInfo, execp, execPromise, getFileImports, getFiles, getLastCommitTime, getPackageJSON, isImportOutsideOfPackage, isImportWithMissingExtension, isInvalidLINCDImport, needsRebuilding, } from './utils.js';
import { spawn as spawnChild } from 'child_process';
import { findNearestPackageJson } from 'find-nearest-package-json';
import { statSync } from 'fs';
import { LinkedFileStorage } from 'lincd/utils/LinkedFileStorage';
// import pkg from 'lincd/utils/LinkedFileStorage';
// const { LinkedFileStorage } = pkg;
// const config = require('lincd-server/site.webpack.config');
import { glob } from 'glob';
import webpack from 'webpack';
import ora from 'ora';
import stagedGitFiles from 'staged-git-files';
let dirname__ = typeof __dirname !== 'undefined'
? __dirname
: //@ts-ignore
dirname(import.meta.url).replace('file:/', '');
var variables = {};
/**
* Prompt user for input
*/
function promptUser(question) {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(chalk.cyan(question), (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
export const createApp = (name_1, ...args_1) => __awaiter(void 0, [name_1, ...args_1], void 0, function* (name, basePath = process.cwd()) {
// If no name provided, prompt for folder name first
if (!name) {
console.log(chalk.blue('\nš Folder name for your app:\n'));
const folderNameInput = yield promptUser('Folder name (e.g., "my-app"): ');
if (!folderNameInput || !folderNameInput.trim()) {
console.warn(chalk.red('Folder name is required. Aborting.'));
return;
}
name = folderNameInput.trim();
}
let { hyphenName, camelCaseName, underscoreName } = setNameVariables(name);
// Prompt user for app configuration
console.log(chalk.blue('\nš Please provide the following information:\n'));
console.log(chalk.gray('(Press Enter to use defaults based on folder name)\n'));
const defaultAppName = name
.split(/[-_\s]+/)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
const defaultAppPrefix = underscoreName;
const defaultAppDomain = hyphenName + '.com';
const appNameInput = yield promptUser(`App Name (display name) [${chalk.gray(defaultAppName)}]: `);
const appPrefixInput = yield promptUser(`App Prefix (short code for data files, e.g., "myapp") [${chalk.gray(defaultAppPrefix)}]: `);
const appDomainInput = yield promptUser(`App Domain [${chalk.gray(defaultAppDomain)}]: `);
const appName = appNameInput || defaultAppName;
const appPrefix = appPrefixInput || defaultAppPrefix;
const appDomain = appDomainInput || defaultAppDomain;
// Set new variables for app configuration
setVariable('app_name', appName);
setVariable('app_prefix', appPrefix);
setVariable('app_domain', appDomain);
let targetFolder = path.join(basePath, hyphenName);
if (!fs.existsSync(targetFolder)) {
fs.mkdirSync(targetFolder);
}
fs.copySync(path.join(dirname__, '..', '..', 'defaults', 'app-with-backend'), targetFolder);
//make sure the data folder exists (even though its empty).. copying empty folders does not work with fs.copySync
fs.mkdirSync(path.join(targetFolder, 'data'), { recursive: true });
fs.mkdirSync(path.join(targetFolder, 'data/uploads/resized'), {
recursive: true,
});
fs.renameSync(path.join(targetFolder, 'gitignore.template'), path.join(targetFolder, '.gitignore'));
fs.renameSync(path.join(targetFolder, 'yarnrc.yml.template'), path.join(targetFolder, '.yarnrc.yml'));
// fs.copySync(path.join(__dirname, '..', 'defaults', 'app'), targetFolder);
log("Creating new LINCD application '" + appName + "'");
//replace variables in some copied files
yield replaceVariablesInFolder(targetFolder);
let hasYarn = yield hasYarnInstalled();
let installCommand = hasYarn
? 'export NODE_OPTIONS="--no-network-family-autoselection" && yarn install'
: 'npm install';
yield execp(`cd ${hyphenName} && ${installCommand}`, true).catch((err) => {
console.warn('Could not install dependencies or start application');
});
log(`Your LINCD App is ready at ${chalk.blueBright(targetFolder)}`, `To start, run\n${chalk.blueBright(`cd ${hyphenName}`)} and then ${chalk.blueBright((hasYarn ? 'yarn' : 'npm') + ' start')}`);
});
function logHelp() {
execp('yarn exec lincd help');
}
function log(...messages) {
messages.forEach((message) => {
console.log(chalk.cyan('Info: ') + message);
});
}
function progressUpdate(message) {
process.stdout.write(' \r');
process.stdout.write(message + '\r');
}
export function warn(...messages) {
messages.forEach((message) => {
console.warn(chalk.redBright('Warning: ') + message);
// console.log(chalk.red(message));
});
}
export function logError(...messages) {
messages.forEach((message) => {
console.error(chalk.redBright('Error: ') + message);
});
}
export function developPackage(target, mode) {
if (!target)
target = 'es6';
if (mode !== 'production')
mode = '';
else if (target !== 'es6')
log('target must be es6 when developing for production');
if (target == 'es5' || target == 'es6') {
// log('> Starting continuous development build for '+target+' target')
log('starting continuous development build');
log('grunt dev' +
(target ? '-' + target : '') +
(mode ? '-' + mode : '') +
' --color');
var command = exec('grunt dev' +
(target ? '-' + target : '') +
(mode ? '-' + mode : '') +
' --color');
command.stdout.pipe(process.stdout);
command.stderr.pipe(process.stderr);
}
else {
console.warn('unknown build target. Use es5 or es6');
}
}
function checkWorkspaces(rootPath, workspaces, res) {
// console.log('checking workspaces at '+rootPath+": "+workspaces.toString());
if (workspaces.packages) {
workspaces = workspaces.packages;
}
workspaces.forEach((workspace) => {
let workspacePath = path.join(rootPath, workspace.replace('/*', ''));
if (workspace.indexOf('/*') !== -1) {
// console.log(workspacePath);
if (fs.existsSync(workspacePath)) {
let folders = fs.readdirSync(workspacePath);
folders.forEach((folder) => {
if (folder !== './' && folder !== '../') {
checkPackagePath(rootPath, path.join(workspacePath, folder), res);
}
});
}
}
else {
checkPackagePath(rootPath, workspacePath, res);
}
});
}
function checkPackagePath(rootPath, packagePath, res) {
let packageJsonPath = path.join(packagePath, 'package.json');
// console.log('checking '+packagePath);
if (fs.existsSync(packageJsonPath)) {
var pack = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
//some packages are not true lincd packages, but we still want them to be re-built automatically. This is what lincd_util is for
if (pack && pack.workspaces) {
checkWorkspaces(packagePath, pack.workspaces, res);
}
else if (pack && pack.lincd === true) {
res.push({
path: packagePath,
packageName: pack.name,
});
}
}
}
export function runOnPackagesGroupedByDependencies(lincdPackages, onBuildStack, onStackEnd, sync = false) {
let dependencies = new Map();
let res, rej;
const deferredPromise = new Promise((resolve, reject) => {
res = resolve;
rej = reject;
});
//get dependencies of each package
let leastDependentPackage;
lincdPackages.forEach((pkg) => {
var pack = getPackageJSON(pkg.path);
if (pack) {
//get lincd related dependencies and get the actual package details from the package map by removing '@dacore/' from the package name
let packageDependencies = Object.keys(pack.dependencies)
.filter((dependency) => lincdPackages.has(dependency))
.map((dependency) => {
return lincdPackages.has(dependency)
? lincdPackages.get(dependency)
: dependency;
});
// console.log(package.packageName,packageDependencies.map())
dependencies.set(pkg, packageDependencies);
}
});
dependencies.forEach((PackageDependencies, pkg) => {
if (!PackageDependencies.some((dependency) => {
return (typeof dependency !== 'string' &&
lincdPackages.has(dependency.packageName));
})) {
leastDependentPackage = pkg;
}
});
let startStack = leastDependentPackage
? [leastDependentPackage]
: [];
const runPackage = (runFunction, pck) => __awaiter(this, void 0, void 0, function* () {
try {
const result = yield runFunction(pck);
done.add(pck);
return result;
}
catch (errorObj) {
if (errorObj.error) {
let { error, stdout, stderr } = errorObj;
warn('Uncaught exception whilst running parallel function on ' +
pck.packageName, (error === null || error === void 0 ? void 0 : error.message) ? error.message : error === null || error === void 0 ? void 0 : error.toString());
}
else {
warn('Uncaught exception whilst running parallel function on ' +
pck.packageName, errorObj === null || errorObj === void 0 ? void 0 : errorObj.toString());
process.exit();
}
// warn(chalk.red(pck.packageName+' failed:'));
// console.log(stdout);
done.add(pck);
return undefined;
}
});
let done = new Set();
let results = [];
let runStack = (stack) => __awaiter(this, void 0, void 0, function* () {
let runFunction = onBuildStack(stack, dependencies);
let stackPromise;
if (sync) {
//build the stack in parallel
stackPromise = Promise.resolve(true);
stack.forEach((pck) => {
stackPromise = stackPromise.then(() => {
return runPackage(runFunction, pck);
});
});
}
else {
//build the stack in parallel
stackPromise = Promise.all(stack.map((pck) => {
return runPackage(runFunction, pck);
}));
}
//wait till stack is completed
let stackResults = yield stackPromise;
results = results.concat(stackResults);
//clear stack for next round
stack = [];
//find those packages who have all their dependencies already built and add them to the stack
lincdPackages.forEach((pkg) => {
let deps = dependencies.get(pkg);
//if the package is not done yet
//but every dependency is now done OR was not something we can build (some @dacore dependencies may not be local)
if (!done.has(pkg) &&
deps.every((dependency) => {
return (typeof dependency !== 'string' &&
(done.has(dependency) || !lincdPackages.has(dependency.packageName)));
})) {
stack.push(pkg);
}
});
if (stack.length <= 0 && done.size < lincdPackages.size) {
console.log(chalk.red('Only ' +
done.size +
' out of ' +
lincdPackages.size +
' packages have been built'));
console.log('ALL remaining packages have dependencies that have not been met. This may point to ' +
chalk.red('circular dependencies.'));
console.log('Already built: ' +
Array.from(done)
.map((p) => chalk.green(p.packageName))
.join(', '));
console.log(chalk.blue('\nTo solve this issue') +
': find the circular dependencies below and fix the dependencies:\n\n');
//TODO: actually find and name the packages that have circular dependencies
// let circular = [];
// lincdPackages.forEach((pkg) => {
// if (!done.has(pkg))
// {
// let deps = dependencies.get(pkg);
// if (deps.some(dependency => {
// //return true if this dependency (indirectly) depends on the package whos' dependency it is
// return hasDependency(dependency,pkg,dependencies)
// }))
// {
// circular.push(pkg);
// }
// process.exit();
// }
// });
lincdPackages.forEach((pkg) => {
let deps = dependencies.get(pkg);
if (!done.has(pkg)) {
console.log(chalk.red(pkg.packageName) +
' has not been built yet. Unbuilt dependencies:\n' +
deps
.filter((dependency) => {
return !Array.from(done).some((p) => {
// console.log(p.packageName,dependency.packageName,p===dependency)
return p === dependency;
});
})
.map((p) => chalk.red('\t- ' +
((p === null || p === void 0 ? void 0 : p.packageName) ? p.packageName : p.toString()) +
'\n'))
.join(' '));
// console.log(chalk.red(pkg.packageName)+' has not been built yet. Built dependencies:\n' + deps.filter(dependency => {
// return Array.from(done).some(p => p.packageName === pkg.packageName)
// }).map(p => chalk.green('\t- '+p.packageName+'\n')).join(" "))
// console.log(chalk.red(pkg.packageName)+' has not been built yet. Built dependencies:\n' + deps.filter(dependency => done.has(pkg)).map(p => chalk.green('\t- '+p.packageName+'\n')).join(" "))
}
});
}
//if more to be built, iterate
if (stack.length > 0) {
return runStack(stack);
}
else {
onStackEnd(dependencies, results.filter(Boolean));
res();
}
});
//starts the process
if (startStack.length === 0) {
// No packages to build, resolve immediately
onStackEnd(dependencies, []);
res();
}
else {
runStack(startStack).catch((err) => {
rej(err);
});
}
return deferredPromise;
}
function hasDependency(pkg, childPkg, dependencies, depth = 1) {
console.log('Does ' + pkg.packageName + ' have dep ' + childPkg.packageName + ' ?');
let deps = dependencies.get(pkg);
if (deps.some((dependency) => {
console.log(dependency.packageName, childPkg.packageName, dependency === childPkg);
if (depth === 2)
return false;
// return dependency === childPkg;
return (dependency === childPkg ||
hasDependency(dependency, childPkg, dependencies, depth++));
})) {
console.log('##YES');
return true;
}
console.log('going up');
return false;
}
/**
* Finds the topmost package.json that could be an APP_ROOT
* Returns null if no app root is found (standalone repo case)
*/
function findAppRoot(startPath = process.cwd()) {
let currentPath = startPath;
let candidateRoots = [];
// Walk up the directory tree
for (let i = 0; i < 10; i++) {
const packageJsonPath = path.join(currentPath, 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
candidateRoots.push({
path: currentPath,
hasWorkspaces: !!packageJson.workspaces,
isLincd: packageJson.lincd === true,
});
}
const parentPath = path.join(currentPath, '..');
// If we've reached the root or haven't moved up
if (parentPath === currentPath) {
break;
}
currentPath = parentPath;
}
// Find the topmost package.json that has workspaces
// Prefer non-lincd packages (app roots) over lincd packages
let appRoot = null;
for (let i = candidateRoots.length - 1; i >= 0; i--) {
const candidate = candidateRoots[i];
if (candidate.hasWorkspaces && !candidate.isLincd) {
appRoot = candidate.path;
break;
}
}
// If no non-lincd workspace found, use the topmost workspace
if (!appRoot) {
for (let i = candidateRoots.length - 1; i >= 0; i--) {
if (candidateRoots[i].hasWorkspaces) {
appRoot = candidateRoots[i].path;
break;
}
}
}
return appRoot;
}
/**
* Filters packages to only include those in the dependency tree of the app root
*/
function filterPackagesByDependencyTree(allPackages, appRootPath) {
const appPackageJson = getPackageJSON(appRootPath);
if (!appPackageJson) {
return allPackages;
}
const relevantPackages = new Map();
const packagesToCheck = new Set();
// Start with direct dependencies from app root
if (appPackageJson.dependencies) {
Object.keys(appPackageJson.dependencies).forEach((dep) => {
if (allPackages.has(dep)) {
packagesToCheck.add(dep);
}
});
}
// Recursively add dependencies
const processedPackages = new Set();
while (packagesToCheck.size > 0) {
const packageName = Array.from(packagesToCheck)[0];
packagesToCheck.delete(packageName);
if (processedPackages.has(packageName)) {
continue;
}
processedPackages.add(packageName);
const packageDetails = allPackages.get(packageName);
if (packageDetails) {
relevantPackages.set(packageName, packageDetails);
// Get this package's dependencies
const packageJson = getPackageJSON(packageDetails.path);
if (packageJson && packageJson.dependencies) {
Object.keys(packageJson.dependencies).forEach((dep) => {
if (allPackages.has(dep) && !processedPackages.has(dep)) {
packagesToCheck.add(dep);
}
});
}
}
}
return relevantPackages;
}
export function buildAll(options) {
console.log('Building all LINCD packages of this repository in order of dependencies');
let lincdPackages = getLocalLincdPackageMap();
const originalPackageCount = lincdPackages.size;
// Check if we're in an app context and filter packages accordingly
const appRoot = findAppRoot();
if (appRoot) {
const appPackageJson = getPackageJSON(appRoot);
// Check if this is an app (not a lincd package itself) with lincd dependencies
const isAppWithLincdDeps = appPackageJson &&
appPackageJson.lincd !== true &&
appPackageJson.dependencies &&
Object.keys(appPackageJson.dependencies).some((dep) => lincdPackages.has(dep));
if (isAppWithLincdDeps) {
debugInfo(chalk.blue(`Found app root at: ${appRoot}`));
const filteredPackages = filterPackagesByDependencyTree(lincdPackages, appRoot);
console.log(chalk.magenta(`Found ${originalPackageCount} total LINCD packages, building only ${filteredPackages.size} that are relevant to this app`));
lincdPackages = filteredPackages;
}
else {
debugInfo(chalk.blue(`Building all ${originalPackageCount} packages from workspace`));
}
}
else {
debugInfo(chalk.blue(`No workspace root found, building all ${originalPackageCount} packages`));
}
let startFrom;
//by default start building
let building = true;
let from = options === null || options === void 0 ? void 0 : options.from;
let sync = (options === null || options === void 0 ? void 0 : options.sync) || false;
// console.log('from', from);
// console.log('sync', sync);
// process.exit();
//option to start from a specific package in the stack
if (from) {
startFrom = from;
//if we have a startFrom, then we havnt started the build process yet
building = startFrom ? false : true;
//clear targets
// target = '';
// target2 = '';
console.log(chalk.blue('Will skip builds until ' + startFrom));
// return async (pkg) => {};
}
// if (target2 == 'from') {
// startFrom = target3;
// //if we have a startFrom, then we havnt started the build process yet
// building = startFrom ? false : true;
//
// //clear targets
// target2 = '';
// target3 = '';
// console.log(chalk.blue('Will skip builds until ' + startFrom));
//
// // return async (pkg) => {};
// }
let done = new Set();
let failedModules = [];
progressUpdate(lincdPackages.size + ' packages left');
let packagesLeft = lincdPackages.size;
// let packagesLeft = lincdPackages.size - done.size;
runOnPackagesGroupedByDependencies(lincdPackages, (packageGroup, dependencies) => {
if (done.size > 0) {
debugInfo(chalk.magenta('\n-------\nThese packages are next, since all their dependencies have now been build:'));
// log(stack);
}
debugInfo('Now building: ' + chalk.blue(packageGroup.map((i) => i.packageName)));
return (pkg) => __awaiter(this, void 0, void 0, function* () {
let command;
let skipping = false;
//if we're skipping builds until a certain package
if (!building) {
//if the package name matches the package we're supposed to start from then start building packages
if (pkg.packageName == startFrom || pkg.packageName == startFrom) {
building = true;
}
//else still waiting for the package
else {
log(chalk.blue('skipping ' + pkg.packageName));
command = Promise.resolve(true);
skipping = true;
}
}
//unless told otherwise, build the package
if (!command) {
command = buildPackage(null, null, path.join(process.cwd(), pkg.path), false);
// command = execPromise(
// 'cd ' + pkg.path + ' && yarn exec lincd build',
// // (target ? ' ' + target : '') +
// // (target2 ? ' ' + target2 : ''),
// false,
// false,
// {},
// false,
// );
log(chalk.cyan('Building ' + pkg.packageName));
process.stdout.write(packagesLeft + ' packages left\r');
}
return command
.then((res) => {
//empty string or true is success
//false is success with warnings
//any other string is the build error text
//undefined result means it failed
// if (res !== '' && res !== true && res !== false) {
if (typeof res === 'undefined') {
failedModules.push(pkg.packageName);
let dependentModules = getDependentPackages(dependencies, pkg);
if (dependentModules.length > 0) {
printBuildResults(failedModules, done);
console.log('Stopping build process because an error occurred whilst building ' +
pkg.packageName +
', which ' +
dependentModules.length +
' other packages depend on.'); //"+dependentModules.map(d => d.packageName).join(", ")));
log('Run ' +
chalk.greenBright(`lincd build-all --from=${pkg.packageName}`) +
' to build only the remaining packages'); //"+dependentModules.map(d => d.packageName).join(", ")));
process.exit(1);
}
}
else {
if (!skipping) {
log(chalk.green('Built ' + pkg.packageName) +
(res === false ? chalk.redBright(' (with warnings)') : ''));
}
done.add(pkg);
packagesLeft--;
// log(chalk.magenta(packagesLeft + ' packages left'));
process.stdout.write(packagesLeft + ' packages left\r');
if (packagesLeft == 0) {
printBuildResults(failedModules, done);
if (failedModules.length > 0) {
process.exit(1);
}
}
return res;
}
})
.catch(({ error, stdout, stderr }) => {
logError(chalk.red('Failed to build ' + pkg.packageName));
console.log(stdout);
process.exit(1);
// let dependentModules = getDependentP
});
//undefined result means it failed
/*if (typeof res === 'undefined')
{
// .catch(({ error,stdout,stderr }) => {
//this prints out the webpack output, including the build errors
// warn('Failed to build ' + pkg.packageName);
// console.log(stdout);
failedModules.push(pkg.packageName);
let dependentModules = getDependentPackages(dependencies,pkg);
if (dependentModules.length > 0)
{
printBuildResults(failedModules,done);
console.log(
'Stopping build process because an error occurred whilst building ' +
pkg.packageName +
', which ' +
dependentModules.length +
' other packages depend on.',
); //"+dependentModules.map(d => d.packageName).join(", ")));
log(
'Run ' +
chalk.greenBright(`lincd build-all --from=${pkg.packageName}`) +
' to build only the remaining packages',
); //"+dependentModules.map(d => d.packageName).join(", ")));
process.exit(1);
}
}
else //true is successful build, false is successful but with warnings
{
//successful build
// })
// .then((res) => {
if (!skipping)
{
log(chalk.green('Built ' + pkg.packageName)+(res === false ? chalk.redBright(' (with warnings)') : ''));
}
done.add(pkg);
packagesLeft--;
// log(chalk.magenta(packagesLeft + ' packages left'));
process.stdout.write(packagesLeft + ' packages left\r');
if (packagesLeft == 0)
{
printBuildResults(failedModules,done);
if (failedModules.length > 0)
{
process.exit(1);
}
}
return res;
}*/
// }).catch(err => {
// console.log(err);
// })
});
}, (dependencies) => {
//if no more packages to build but we never started building...
if (!building) {
console.log(chalk.red('Could not find the package to start from. Please provide a correct package name or package name to build from'));
}
else {
//Detecting cyclical dependencies that caused some packages not to be build
let first = true;
lincdPackages.forEach((pkg) => {
if (!done.has(pkg)) {
let deps = dependencies.get(pkg);
if (first) {
console.log(chalk.red('CYCLICAL DEPENDENCIES? - could not build some packages because they depend on each other.'));
first = false;
}
//print the cyclical dependencies
console.log(chalk.red(pkg.packageName) +
' depends on ' +
deps
.filter((dependency) => {
return typeof dependency !== 'string';
})
.map((d) => {
return done.has(d)
? d.packageName
: chalk.red(d.packageName);
})
.join(', '));
//also print some information why these packages have not been moved into the stack
let stringDependencies = deps.filter((d) => typeof d === 'string');
if (stringDependencies.length > 0) {
console.log(chalk.red('And it depends on these package(s) - which seem not to be proper packages :' +
stringDependencies.join(', ')));
console.log(chalk.red('Could you remove this from dependencies? Should it be a devDependency?'));
}
}
});
}
}, sync);
}
function getDependentPackages(dependencies, pkg) {
let dependentModules = [];
dependencies.forEach((dModuleDependencies, dModule) => {
if (dModuleDependencies.indexOf(pkg) !== -1) {
dependentModules.push(dModule);
}
});
return dependentModules;
}
/**
* Returns a map of the packages that this repository manages (so no packages found through the workspaces who's path contains ../ )
* @param rootPath
*/
function getLocalLincdPackageMap(rootPath = './') {
let map = new Map();
getLincdPackages(rootPath).forEach((pkg) => {
if (pkg.path.indexOf('../') === -1 && pkg.path.indexOf('..\\') === -1) {
// console.log(package.path);
map.set(pkg.packageName, pkg);
}
});
return map;
}
function getLocalLincdModules(rootPath = './') {
return getLincdPackages(rootPath).filter((pkg) => {
return pkg.path.indexOf('..\\') === -1;
});
}
export function getLincdPackages(rootPath = process.cwd()) {
let pack = getPackageJSON(rootPath);
if (!pack || !pack.workspaces) {
const originalRoot = rootPath;
for (let i = 0; i <= 3; i++) {
rootPath = path.join(originalRoot, ...Array(i).fill('..'));
pack = getPackageJSON(rootPath);
if (pack && pack.workspaces) {
// log('Found workspace at '+packagePath);
break;
}
}
}
if (!pack || !pack.workspaces) {
warn(chalk.red('Could not find package workspaces. Make sure you run this command from a yarn workspace.'));
logHelp();
process.exit();
}
// console.log(pack.workspaces);
let res = [];
checkWorkspaces(rootPath, pack.workspaces, res);
return res;
}
function setVariable(name, replacement) {
//prepare name for regexp
name = name.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
variables[name] = replacement;
}
var replaceVariablesInFile = (filePath) => __awaiter(void 0, void 0, void 0, function* () {
var fileContent = yield fs.readFile(filePath, 'utf8').catch((err) => {
console.warn(chalk.red('Could not read file ' + filePath));
});
if (fileContent) {
var newContent = replaceCurlyVariables(fileContent);
return fs.writeFile(filePath, newContent);
}
else {
return Promise.resolve();
}
});
var replaceCurlyVariables = function (string) {
// var reg = new RegExp('\\$\\{'+key+'\\}','g');
for (var key in variables) {
string = string.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), variables[key]);
}
return string;
};
const capitalize = (str) => str.charAt(0).toUpperCase() + str.toLowerCase().slice(1);
const camelCase = (str) => {
let string = str.replace(/[^A-Za-z0-9]/g, ' ').split(' ');
if (string.length > 1) {
return string.reduce((result, word) => result + capitalize(word));
}
return str;
};
export const createOntology = (prefix_1, uriBase_1, ...args_2) => __awaiter(void 0, [prefix_1, uriBase_1, ...args_2], void 0, function* (prefix, uriBase, basePath = process.cwd()) {
if (!prefix) {
console.warn('Please provide a suggested prefix as the first argument');
return;
}
let sourceFolder = getSourceFolder(basePath);
let targetFolder = ensureFolderExists(sourceFolder, 'ontologies');
if (!uriBase) {
uriBase = 'http://lincd.org/ont/' + prefix + '/';
}
setVariable('uri_base', uriBase);
let { hyphenName, camelCaseName, underscoreName } = setNameVariables(prefix);
//copy ontology accessor file
log("Creating files for ontology '" + prefix + "'");
let targetFile = path.join(targetFolder, hyphenName + '.ts');
fs.copySync(path.join(dirname__, '..', '..', 'defaults', 'package', 'src', 'ontologies', 'example-ontology.ts'), targetFile);
//copy data files
let targetDataFile = path.join(targetFolder, '..', 'data', hyphenName + '.json');
let targetDataFile2 = path.join(targetFolder, '..', 'data', hyphenName + '.json.d.ts');
fs.copySync(path.join(dirname__, '..', '..', 'defaults', 'package', 'src', 'data', 'example-ontology.json'), targetDataFile);
fs.copySync(path.join(dirname__, '..', '..', 'defaults', 'package', 'src', 'data', 'example-ontology.json.d.ts'), targetDataFile2);
yield replaceVariablesInFiles(targetFile, targetDataFile, targetDataFile2);
log(`Prepared a new ontology data files in ${chalk.magenta(targetDataFile.replace(basePath, ''))}`, `And an ontology accessor file in ${chalk.magenta(targetFile.replace(basePath, ''))}`);
//if this is not a lincd app (but a lincd package instead)
if (!sourceFolder.includes('frontend')) {
//then also add an import to index
let indexPath = addLineToIndex(`import './ontologies/${hyphenName}.js';`, 'ontologies');
log(`Added an import of this file from ${chalk.magenta(indexPath)}`);
}
});
const addLineToIndex = function (line, insertMatchString, root = process.cwd(), insertAtStart = false) {
//import ontology in index
let indexPath = ['index.ts', 'index.tsx']
.map((f) => path.join(root, 'src', f))
.find((indexFileName) => {
return fs.existsSync(indexFileName);
});
if (indexPath) {
let indexContents = fs.readFileSync(indexPath, 'utf-8');
let lines = indexContents.split(/\n/g);
let newContents;
for (var key in lines) {
//if the match string is found
if (lines[key].indexOf(insertMatchString) !== -1) {
//add the new line after this line
lines[key] += `\n${line}`;
newContents = lines.join('\n');
// log("Found at "+key,lines,newContents);
break;
}
}
if (!newContents) {
if (insertAtStart) {
newContents = `${line}\n${indexContents}`;
}
else {
newContents = `${indexContents}\n${line}`;
}
// log("Added at end",newContents);
}
fs.writeFileSync(indexPath, newContents);
}
return indexPath;
};
const replaceVariablesInFiles = function (...files) {
return Promise.all(files.map((file) => {
return replaceVariablesInFile(file);
}));
};
const replaceVariablesInFolder = function (folder) {
return __awaiter(this, void 0, void 0, function* () {
//get all files in folder, including files that start with a dot
try {
const files = yield glob(folder + '/**/*', { dot: true, nodir: true });
console.log('Replacing variables in files', files.join(', '));
yield Promise.all(files.map((file) => {
return replaceVariablesInFile(file);
}));
}
catch (err) {
console.log('Error', err);
throw err;
}
});
};
const replaceVariablesInFilesWithRoot = function (root, ...files) {
return replaceVariablesInFiles(...files.map((f) => path.join(root, f)));
};
const hasYarnInstalled = function () {
return __awaiter(this, void 0, void 0, function* () {
let version = (yield execPromise('yarn --version').catch((err) => {
console.log('yarn probably not working');
return '';
}));
return version.toString().match(/[0-9]+/);
});
};
const ensureFolderExists = function (...folders) {
let target;
folders.forEach((folder) => {
target = target ? path.join(target, folder) : path.join(folder);
if (!fs.existsSync(target)) {
fs.mkdirSync(target);
}
});
return target;
/*let targetFolder = path.join(...folders);
let parentDirectory = folders.slice(0, folders.length - 1);
if (!fs.existsSync(targetFolder)) {
if (fs.existsSync(path.join(...parentDirectory))) {
fs.mkdirSync(targetFolder);
} else {
warn(
`Please run this command from the root of your package. This command expects ${parentDirectory.toString()} to exists from that folder`,
);
}
}
return targetFolder;*/
};
export const setNameVariables = function (name) {
let hyphenName = name.toLowerCase().replace(/[-_\s]+/g, '-');
let camelCaseName = camelCase(name); //some-package --> someModule
let underscoreName = name.replace(/[-\s]+/g, '_');
let plainName = name.replace(/[-\s]+/g, '');
//longer similar variables names should come before the shorter ones
setVariable('underscore_name', underscoreName);
setVariable('hyphen_name', hyphenName);
setVariable('camel_name', camelCaseName);
setVariable('name', name);
setVariable('plain_name', plainName);
return { hyphenName, camelCaseName, underscoreName, plainName };
};
function getSourceFolder(basePath = process.cwd()) {
//LINCD App
if (fs.existsSync(path.join(basePath, 'frontend', 'src'))) {
return path.join(basePath, 'frontend', 'src');
}
//LINCD package
if (fs.existsSync(path.join(basePath, 'src'))) {
return path.join(basePath, 'src');
}
else {
console.warn('Cannot find source folder');
return path.join(basePath, 'src');
}
}
/**
* get __dirname for either ESM/CJS
*/
export const getScriptDir = () => {
return dirname__;
// // @ts-ignore
// if (typeof __dirname !== 'undefined')
// {
// // @ts-ignore
// return __dirname;
// }
// else
// {
// // @ts-ignore
// return dirname(import.meta.url).replace('file:/','');
// }
};
export const createShape = (name_2, ...args_3) => __awaiter(void 0, [name_2, ...args_3], void 0, function* (name, basePath = process.cwd()) {
let sourceFolder = getSourceFolder(basePath);
let targetFolder = ensureFolderExists(sourceFolder, 'shapes');
let { hyphenName, camelCaseName, underscoreName } = setNameVariables(name);
//copy default shape file
// log("Creating files for shape '" + name + "'");
let targetFile = path.join(targetFolder, hyphenName + '.ts');
fs.copySync(path.join(getScriptDir(), '..', '..', 'defaults', 'shape.ts'), targetFile);
//replace variables in some of the copied files
yield replaceVariablesInFiles(targetFile);
log(`Created a new shape class template in ${chalk.magenta(targetFile.replace(basePath, ''))}`);
//if this is NOT a lincd app (but a lincd package)
let indexPath;
if (!sourceFolder.includes('frontend')) {
indexPath = addLineToIndex(`import './shapes/${hyphenName}.js';`, 'shapes');
log(`Added an import of this file from ${chalk.magenta(indexPath)}`);
}
});
export const createSetComponent = (name_3, ...args_4) => __awaiter(void 0, [name_3, ...args_4], void 0, function* (name, basePath = process.cwd()) {
let targetFolder = ensureFolderExists(basePath, 'src', 'components');
let { hyphenName, camelCaseName, underscoreName } = setNameVariables(name);
//copy default shape file
log("Creating files for set component '" + name + "'");
let targetFile = path.join(targetFolder, hyphenName + '.tsx');
fs.copySync(path.join(getScriptDir(), '..', '..', 'defaults', 'set-component.tsx'), targetFile);
let targetFile2 = path.join(targetFolder, hyphenName + '.scss');
fs.copySync(path.join(getScriptDir(), '..', '..', 'defaults', 'component.scss'), targetFile2);
//replace variables in some of the copied files
yield replaceVariablesInFiles(targetFile, targetFile2);
let indexPath = addLineToIndex(`import './components/${hyphenName}.js';`, 'components');
log(`Created a new set component in ${chalk.magenta(targetFile.replace(basePath, ''))}`, `Created a new stylesheet in ${chalk.magenta(targetFile2.replace(basePath, ''))}`, `Added an import of this file from ${chalk.magenta(indexPath)}`);
});
export const createComponent = (name_4, ...args_5) => __awaiter(void 0, [name_4, ...args_5], void 0, function* (name, basePath = process.cwd()) {
let sourceFolder = getSourceFolder(basePath);
let targetFolder = ensureFolderExists(sourceFolder, 'components');
let { hyphenName, camelCaseName, underscoreName } = setNameVariables(name);
//copy default shape file
log("Creating files for component '" + name + "'");
let targetFile = path.join(targetFolder, hyphenName + '.tsx');
fs.copySync(path.join(getScriptDir(), '..', 'defaults', 'component.tsx'), targetFile);
let targetFile2 = path.join(targetFolder, hyphenName + '.scss');
fs.copySync(path.join(getScriptDir(), '..', 'defaults', 'component.scss'), targetFile2);
//replace variables in some of the copied files
yield replaceVariablesInFiles(targetFile, targetFile2);
log(`Created a new component template in ${chalk.magenta(targetFile.replace(basePath, ''))}`, `Created component stylesheet template in ${chalk.magenta(targetFile2.replace(basePath, ''))}`);
//if this is not a lincd app (but a lincd package instead)
if (!sourceFolder.includes('frontend')) {
//then also add an import to index
let indexPath = addLineToIndex(`import './components/${hyphenName}.js';`, 'components');
log(`Added an import of this file from ${chalk.magenta(indexPath)}`);
}
});
//read the source of all ts/tsx files in the src folder
//if there is an import that imports a lincd package with /src/ in it, then warn
//if there is an import that imports something from outside the src folder, then warn
export const checkImports = (...args_6) => __awaiter(void 0, [...args_6], void 0, function* (sourceFolder = getSourceFolder(), depth = 0, // Used to check if the import is outside the src folder
invalidImports = new Map()) {
const dir = fs.readdirSync(sourceFolder);
// Start checking each file in the source folder
for (const file of dir) {
const filename = path.join(sourceFolder, file);
// File is either a directory, or not a .ts(x)
// INFO: For future use - if this part fails, it could be due to user permissions
// i.e. the program not having access to check the file metadata
if (!filename.match(/\.tsx?$/)) {
try {
if (statSync(filename).isDirectory()) {
yield checkImports(filename, depth + 1, invalidImports);
}
else {
// Ignore all files that aren't one of the following:
// - .ts
// - .tsx
continue;
}
}
catch (e) {
console.log(e);
}
}
const allImports = yield getFileImports(filename);
if (!invalidImports.has(filename)) {
invalidImports.set(filename, []);
}
allImports.forEach((i) => {
if (isImportOutsideOfPackage(i, depth)) {
invalidImports.get(filename).push({
type: 'outside_package',
importPath: i,
});
}
if (isInvalidLINCDImport(i, depth)) {
invalidImports.get(filename).push({
type: 'lincd',
importPath: i,
});
}
if (isImportWithMissingExtension(i)) {
invalidImports.get(filename).push({
type: 'missing_extension',
importPath: i,
});
}
});
}
let res = '';
//check if invalidImports has any
let flat = [...invalidImports.values()].flat();
// All recursion must have finished, display any errors
if (depth === 0 && flat.length > 0) {
res += chalk.red('Invalid imports found.\n');
invalidImports.forEach((value, key) => {
// res += '- '+chalk.blueBright(key.split('/').pop()) + ':\n';
value.forEach(({ type, importPath }) => {
let message = key.split('/').pop() + " imports from '" + importPath + "'";
if (type === 'outside_package') {
message += ' which is outside the package source root';
}
if (type === 'lincd') {
message +=
' which should not contain /src/ or /lib/ in the import path';
}
if (type === 'missing_extension') {
message +=
' which should end with a file extension. Like .js or .scss';
}
res += chalk.red(message + '\n');
});
});
throw res;
// process.exit(1);
}
else if (depth === 0 && invalidImports.size === 0) {
// console.info('All imports OK');
// process.exit(0);
return true;
}
});
export const depCheckStaged = () => __awaiter(void 0, void 0, void 0, function* () {
console.log('Checking dependencies of staged files');
stagedGitFiles(function (err, results) {
return __awaiter(this, void 0, void 0, function* () {
const packages = new Set();
yield Promise.all(results.map((file) => __awaiter(this, void 0, void 0, function* () {
// console.log('STAGED: ', file.filename);
let root = yield findNearestPackageJson(file.filename);
packages.add(root.path);
})));
[...packages].forEach((packageRoot) => {
const pack = JSON.parse(fs.readFileSync(packageRoot, 'utf8'));
const srcPath = packageRoot.replace('package.json', '');
console.log('Checking dependencies of ' + chalk.blue(pack.name) + ':');
return depCheck(process.cwd() + '/' + srcPath);
// console.log('check dependencies of ' + pack.name);
//
// console.log('ROOT of ' + file.filename + ': ' + root.path);
// console.log('ROOT of ' + file.filename + ': ' + root.data);
});
});
});
});
export const depCheck = (...args_7) => __awaiter(void 0, [...args_7], void 0, function* (packagePath = process.cwd()) {
// log('Checking depencies of ' + chalk.cyan(packagePath));
return new Promise((resolve, reject) => {
depcheck(packagePath, {}, (results) => {
if (results.missing) {
let lincdPackages = getLocalLincdModules(packagePath);
let missing = Object.keys(results.missing);
//filter out missing types, if it builds we're not too concerned about that at the moment?
//especially things like @types/react, @types/react-dom, @types/node (they are added elsewhere?)