piral-cli
Version:
The standard CLI for creating and building a Piral instance or a Pilet.
538 lines (461 loc) • 16.4 kB
text/typescript
import { transpileModule, ModuleKind, ModuleResolutionKind, ScriptTarget, JsxEmit, version } from 'typescript';
import { join, resolve, basename, dirname, extname } from 'path';
import { exists, lstat, unlink, statSync } from 'fs';
import { mkdtemp, mkdir, constants } from 'fs';
import { writeFile, readFile, readdir, copyFile } from 'fs';
import { packageJson, piletJson } from './constants';
import { log } from './log';
import { deepMerge } from './merge';
import { computeHash } from './hash';
import { ForceOverwrite } from './enums';
import { promptConfirm } from './interactive';
import { glob, rimraf } from '../external';
function promptOverwrite(file: string) {
const message = `The file ${file} exists already. Do you want to overwrite it?`;
return promptConfirm(message, false);
}
function isFile(file: string) {
return statSync(file).isFile();
}
export interface Destination {
outDir: string;
outFile: string;
}
export function getDestination(entryFiles: string, target: string): Destination {
const isdir = extname(target) !== '.html';
if (isdir) {
return {
outDir: target,
outFile: basename(entryFiles),
};
} else {
return {
outDir: dirname(target),
outFile: basename(target),
};
}
}
export async function removeAny(target: string) {
const isDir = await checkIsDirectory(target);
if (isDir) {
await removeDirectory(target);
} else {
await removeFile(target);
}
}
export function removeDirectory(targetDir: string) {
log('generalDebug_0003', `Removing the directory "${targetDir}" ...`);
return rimraf(targetDir);
}
export async function createDirectory(targetDir: string) {
try {
log('generalDebug_0003', `Trying to create "${targetDir}" ...`);
await new Promise<void>((resolve, reject) => {
mkdir(targetDir, { recursive: true }, (err) => (err ? reject(err) : resolve()));
});
return true;
} catch (e) {
log('cannotCreateDirectory_0044');
log('generalDebug_0003', `Error while creating ${targetDir}: ${e}`);
return false;
}
}
export async function getEntryFiles(content: string, basePath: string) {
log('generalDebug_0003', `Extract entry files from "${basePath}".`);
const matcher = /<script\s.*?src=(?:"(.*?)"|'(.*?)'|([^\s>]*)).*?>/gi;
const results: Array<string> = [];
let result: RegExpExecArray = undefined;
while ((result = matcher.exec(content))) {
const src = result[1] || result[2] || result[3];
log('generalDebug_0003', `Found potential entry file "${src}".`);
const filePath = resolve(basePath, src);
const exists = await checkExists(filePath);
if (exists) {
results.push(filePath);
}
}
return results;
}
export function makeTempDir(prefix: string) {
return new Promise<string>((resolve, reject) =>
mkdtemp(prefix, (err, folder) => {
if (err) {
reject(err);
} else {
resolve(folder);
}
}),
);
}
export function checkExists(target: string) {
return new Promise<boolean>((resolve) => {
if (target !== undefined) {
exists(target, resolve);
} else {
resolve(false);
}
});
}
export async function checkExistingDirectory(target: string) {
log('generalDebug_0003', `Checking directory "${target}" ...`);
if (await checkExists(target)) {
log('generalDebug_0003', `Target exists, but not yet clear if directory.`);
return await checkIsDirectory(target);
}
return false;
}
export function checkIsDirectory(target: string) {
return new Promise<boolean>((resolve) => {
lstat(target, (err, stats) => {
if (err) {
resolve(extname(target) === '');
} else {
resolve(stats.isDirectory());
}
});
});
}
export function getFileNames(target: string) {
return new Promise<Array<string>>((resolve, reject) => {
readdir(target, (err, files) => (err ? reject(err) : resolve(files)));
});
}
export async function findFile(
topDir: string,
fileName: string | Array<string>,
stopDir = resolve(topDir, '/'),
): Promise<string> {
const fileNames = Array.isArray(fileName) ? fileName : [fileName];
for (const fn of fileNames) {
const path = join(topDir, fn);
const exists = await checkExists(path);
if (exists) {
return path;
}
}
if (topDir !== stopDir) {
const parentDir = resolve(topDir, '..');
return await findFile(parentDir, fileNames, stopDir);
}
return undefined;
}
interface AnyPattern {
original: string;
patterns: Array<string>;
}
function matchPattern(baseDir: string, pattern: string) {
return new Promise<Array<string>>((resolve, reject) => {
glob(
pattern,
{
cwd: baseDir,
nodir: true,
absolute: true,
},
(err, files) => {
if (err) {
reject(err);
} else {
resolve(files);
}
},
);
});
}
async function matchAnyPattern(baseDir: string, pattern: AnyPattern) {
const matches = await Promise.all(pattern.patterns.map((pattern) => matchPattern(baseDir, pattern)));
return {
pattern: pattern.original,
results: matches.reduce((agg, curr) => [...agg, ...curr], []),
};
}
const preferences = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.esm', '.es', '.es6', '.html'];
export async function matchAnyPilet(baseDir: string, patterns: Array<string>) {
const matches: Array<string> = [];
const pilets: Array<string> = [];
const matched = (name: string, path: string) => {
pilets.push(name);
matches.push(path);
};
const exts = preferences.map((s) => s.substring(1)).join(',');
const allPatterns = patterns.reduce<Array<AnyPattern>>((agg, curr) => {
const patterns = [];
if (/[a-zA-Z0-9\-\*]$/.test(curr) && !preferences.find((ext) => curr.endsWith(ext))) {
patterns.push(curr, `${curr}.{${exts}}`, `${curr}/${packageJson}`, `${curr}/${piletJson}`);
} else if (curr.endsWith('/')) {
patterns.push(`${curr}index.{${exts}}`, `${curr}${packageJson}`, `${curr}${piletJson}`);
} else if (curr === '.' || curr === '..') {
patterns.push(`${curr}/index.{${exts}}`, `${curr}/${packageJson}`, `${curr}/${piletJson}`);
} else {
patterns.push(curr);
}
agg.push({ original: curr, patterns });
return agg;
}, []);
await Promise.all(
allPatterns.map((patterns) =>
matchAnyPattern(baseDir, patterns).then(async ({ results, pattern }) => {
if (!results.length) {
log('generalDebug_0003', `Found no potential entry points using "${pattern}".`);
} else {
//TODO -> shouldn't take the first one,
// should be the first one, yes, but, PER pilet
// so that multiple pilets can be considered, too
log('generalDebug_0003', `Found ${results.length} potential entry points in "${pattern}".`);
for (const result of results) {
const fileName = basename(result);
if (fileName === packageJson) {
log('generalDebug_0003', `Entry point is a "${packageJson}" and needs further inspection.`);
const targetDir = dirname(result);
const { source, name } = await readJson(targetDir, fileName);
if (!pilets.includes(name)) {
if (typeof source === 'string') {
log('generalDebug_0003', `Found a "source" field with value "${source}".`);
const target = resolve(targetDir, source);
const exists = await checkExists(target);
if (exists) {
log('generalDebug_0003', `Taking existing target as "${target}".`);
matched(name, target);
} else {
log('generalDebug_0003', `Source target "${target}" does not exist. Skipped.`);
}
} else {
log('generalDebug_0003', `No "source" field found. Trying combinations in "src".`);
const files = await matchPattern(targetDir, `src/index.{${exts}}`);
if (files.length > 0) {
log('generalDebug_0003', `Found a result; taking "${files[0]}".`);
matched(name, files[0]);
} else {
log('generalDebug_0003', `Found no results in "src". Skipped.`);
}
}
}
} else {
const packageJsonPath = await findFile(result, packageJson);
if (packageJsonPath) {
const targetDir = dirname(packageJsonPath);
const { name } = await readJson(targetDir, packageJson);
if (!pilets.includes(name)) {
log('generalDebug_0003', `Entry point result is "${result}".`);
matched(name, result);
}
} else {
log('generalDebug_0003', `Could not find "${packageJson}" for entry "${result}". Skipping.`);
}
}
}
}
}),
),
);
return matches;
}
export function matchFiles(baseDir: string, pattern: string) {
return new Promise<Array<string>>((resolve, reject) => {
glob(
pattern,
{
cwd: baseDir,
absolute: true,
dot: true,
},
(err, files) => {
if (err) {
reject(err);
} else {
resolve(files.filter(isFile));
}
},
);
});
}
export async function createFileIfNotExists(
targetDir: string,
fileName: string,
content: Buffer | string,
forceOverwrite = ForceOverwrite.no,
) {
const targetFile = join(targetDir, fileName);
log('generalDebug_0003', `Checking if file "${targetFile}" exists ...`);
const exists = await checkExists(targetFile);
if (
!exists ||
forceOverwrite === ForceOverwrite.yes ||
(forceOverwrite === ForceOverwrite.prompt && (await promptOverwrite(targetFile)))
) {
await createDirectory(dirname(targetFile));
log('generalDebug_0003', `Creating file "${targetFile}" ...`);
if (typeof content === 'string') {
await writeText(targetDir, fileName, content);
} else {
await writeBinary(targetDir, fileName, content);
}
}
}
export async function updateExistingFile(targetDir: string, fileName: string, content: string) {
const targetFile = join(targetDir, fileName);
log('generalDebug_0003', `Checking if file "${targetFile}" exists ...`);
const exists = await checkExists(targetFile);
if (exists) {
log('generalDebug_0003', `Updating file "${targetFile}" ...`);
await new Promise<void>((resolve, reject) => {
writeFile(targetFile, content, 'utf8', (err) => (err ? reject(err) : resolve()));
});
}
}
export async function getHash(targetFile: string) {
return new Promise<string>((resolve) => {
readFile(targetFile, (err, c) => (err ? resolve(undefined) : resolve(computeHash(c))));
});
}
export async function mergeWithJson<T>(targetDir: string, fileName: string, newContent: T) {
const targetFile = join(targetDir, fileName);
const content = await new Promise<string>((resolve) => {
readFile(targetFile, 'utf8', (err, c) => (err ? resolve('{}') : resolve(c)));
});
const originalContent = JSON.parse(content);
return deepMerge(originalContent, newContent);
}
export async function readJson<T = any>(targetDir: string, fileName: string, defaultValue = {}) {
const targetFile = join(targetDir, fileName);
const content = await new Promise<string>((resolve) => {
readFile(targetFile, 'utf8', (err, c) => (err ? resolve('') : resolve(c)));
});
if (content) {
try {
return JSON.parse(content) as T;
} catch (ex) {
log('generalError_0002', `Invalid JSON found in file "${fileName}" at "${targetDir}".`);
}
}
return defaultValue as T;
}
export function readBinary(targetDir: string, fileName: string) {
const targetFile = join(targetDir, fileName);
return new Promise<Buffer>((resolve) => {
readFile(targetFile, (err, c) => (err ? resolve(undefined) : resolve(c)));
});
}
export function readText(targetDir: string, fileName: string) {
const targetFile = join(targetDir, fileName);
return new Promise<string>((resolve) => {
readFile(targetFile, 'utf8', (err, c) => (err ? resolve(undefined) : resolve(c)));
});
}
export function writeJson<T = any>(targetDir: string, fileName: string, data: T, beautify = false) {
const text = beautify ? JSON.stringify(data, undefined, 2) : JSON.stringify(data);
return writeText(targetDir, fileName, text + '\n');
}
export function writeText(targetDir: string, fileName: string, content: string) {
const data = Buffer.from(content, 'utf8');
return writeBinary(targetDir, fileName, data);
}
export function writeBinary(targetDir: string, fileName: string, data: Buffer) {
const targetFile = join(targetDir, fileName);
return new Promise<void>((resolve, reject) => {
writeFile(targetFile, data, (err) => (err ? reject(err) : resolve()));
});
}
export async function updateExistingJson<T>(targetDir: string, fileName: string, newContent: T) {
const content = await mergeWithJson(targetDir, fileName, newContent);
const text = JSON.stringify(content, undefined, 2);
await updateExistingFile(targetDir, fileName, text + '\n');
}
export async function copy(source: string, target: string, forceOverwrite = ForceOverwrite.no): Promise<boolean> {
await createDirectory(dirname(target));
try {
const flag = forceOverwrite === ForceOverwrite.yes ? 0 : constants.COPYFILE_EXCL;
const isDir = await checkIsDirectory(source);
if (isDir) {
const files = await getFileNames(source);
const results = await Promise.all(
files.map((file) => copy(resolve(source, file), resolve(target, file), forceOverwrite)),
);
return results.every(Boolean);
} else {
await new Promise<void>((resolve, reject) => {
copyFile(source, target, flag, (err) => (err ? reject(err) : resolve()));
});
return true;
}
} catch (e) {
if (forceOverwrite === ForceOverwrite.prompt) {
const shouldOverwrite = await promptOverwrite(target);
if (shouldOverwrite) {
return await copy(source, target, ForceOverwrite.yes);
}
} else {
log('didNotOverWriteFile_0045', target);
}
}
return false;
}
/**
* @deprecated Will be removed with v1. Please use "removeFile".
*/
export function remove(target: string) {
return removeFile(target);
}
export function removeFile(target: string) {
return new Promise<void>((resolve, reject) => {
unlink(target, (err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
});
}
export async function move(source: string, target: string, forceOverwrite = ForceOverwrite.no) {
const dir = await checkIsDirectory(target);
if (dir) {
const file = basename(source);
target = resolve(target, file);
}
const success = await copy(source, target, forceOverwrite);
if (success) {
await removeFile(source);
return target;
}
return source;
}
function isVersion5OrHigher() {
const currentMajor = parseInt(version.split('.').shift());
return currentMajor >= 5;
}
export async function getSourceFiles(entry: string) {
const dir = dirname(entry);
log('generalDebug_0003', `Trying to get source files from "${dir}" ...`);
const files = await matchFiles(dir, '**/*.?(jsx|tsx|js|ts)');
return files.map((path) => {
const directory = dirname(path);
const name = basename(path);
return {
path,
directory,
name,
async read() {
const content = await readText(directory, name);
if (name.endsWith('.ts') || name.endsWith('.tsx')) {
return transpileModule(content, {
fileName: path,
moduleName: name,
compilerOptions: {
allowJs: true,
skipLibCheck: true,
declaration: false,
sourceMap: false,
checkJs: false,
jsx: JsxEmit.React,
module: ModuleKind.ESNext,
moduleResolution: isVersion5OrHigher() ? ModuleResolutionKind.Bundler : ModuleResolutionKind.Node10,
target: ScriptTarget.ESNext,
},
}).outputText;
}
return content;
},
};
});
}