@neodx/vfs
Version:
Simple virtual file system - working dir context, lazy changes, different modes, integrations and moreover
385 lines (376 loc) • 12.8 kB
JavaScript
;
var colors = require('@neodx/colors');
var std = require('@neodx/std');
var shared = require('@neodx/std/shared');
var pathe = require('pathe');
/**
* In-memory VFS backend.
* Useful for testing, emulating file system, etc.
*/ function createInMemoryBackend(root = '/', initializer = {}) {
// generate new implementation based on old one
const store = new Map(
std
.entries(createInMemoryFilesRecord(initializer, root))
.map(([path, content]) => [path, Buffer.from(content)])
);
const deleted = new Set();
const isFile = path => store.has(path);
const isDir = path => !isFile(path) && Array.from(store.keys()).some(pathStartsBy(path));
const deletePath = path => {
store.delete(path);
deleted.add(path);
};
return {
read(path) {
return store.get(path) ?? null;
},
isFile,
isDir,
exists(path) {
return isFile(path) || isDir(path);
},
readDir(path) {
return std
.uniq(
Array.from(store.keys())
.filter(pathStartsBy(path))
.map(name => name.split(withTrailingSlash(path))[1].split('/')[0])
)
.map(name => createInMemoryDirent(name, isFile(pathe.join(path, name))));
},
write(path, content) {
store.set(path, Buffer.from(content));
while (path !== '/') {
deleted.delete(path);
path = pathe.dirname(path);
}
},
delete(path) {
deletePath(path);
for (const name of store.keys()) {
if (pathStartsWith(name, path)) {
deletePath(name);
}
}
},
__: {
kind: 'in-memory',
getStore: () => new Map(store),
getDeleted: () => new Set(deleted)
}
};
}
const createInMemoryDirent = (name, file, symlink = false) => ({
isFile: () => file,
isDirectory: () => !file,
isSymbolicLink: () => symlink,
name
});
const withTrailingSlash = path => (path.endsWith('/') ? path : `${path}/`);
const pathStartsWith = (fullPath, basePath) => fullPath.startsWith(withTrailingSlash(basePath));
const pathStartsBy = basePath => fullPath => pathStartsWith(fullPath, basePath);
/**
* @param initializer Virtual files tree
* @param base Root path
* @example
* ```ts
* createInMemoryFilesRecord({
* "package.json": "{...}",
* "src": {
* "foo": {
* "bar.ts": "export const a = 1"
* "baz.ts": "export const b = 2"
* },
* "index.ts": "export const c = 3"
* }
* });
* // {
* // "package.json": "{...}",
* // "src/foo/bar.ts": "export const a = 1",
* // "src/foo/baz.ts": "export const b = 2",
* // "src/index.ts": "export const c = 3"
* // }
* ```
*/ function createInMemoryFilesRecord(initializer, base = '') {
const result = {};
for (const [name, value] of std.entries(initializer)) {
const path = pathe.join(base, name);
if (std.isTypeOfString(value)) result[path] = value;
else Object.assign(result, createInMemoryFilesRecord(value, path));
}
return result;
}
const linkError = (error, cause) =>
std.isTypeOfString(error)
? new Error(error, {
cause
})
: error ?? cause;
const timeDisplay = () => {
const start = Date.now();
return () => formatTimeMs(Date.now() - start);
};
const formatTimeMs = time => {
const showSeconds = time >= 100;
const timeValue = showSeconds ? time / 1000 : time;
return timeValue
.toLocaleString('en', {
style: 'unit',
unit: showSeconds ? 'second' : 'millisecond',
unitDisplay: 'narrow'
})
.padEnd(showSeconds ? 6 : 4);
};
const formatList = (list, max = 4) => {
const visible = std.compact(list);
const extra = visible.slice(max).length;
return visible.length > 0
? listFormatter.format(extra ? visible.slice(0, max - 1).concat(`${extra} more`) : visible)
: 'none';
};
const listFormatter = new Intl.ListFormat('en', {
style: 'short',
type: 'unit'
});
function createTaskRunner({ log } = {}) {
return {
task: (name, handler, { mapError, invariant: invariantMessage, mapSuccessMessage } = {}) => {
async function task(...args) {
const printAllTime = timeDisplay();
try {
const result = await handler(...args);
if (mapSuccessMessage) {
log?.debug(
`${colors.colors.gray(`[${name}] ${printAllTime()}`)} ${mapSuccessMessage(result, ...args)}`
);
}
if (invariantMessage) {
std.invariant(
result,
std.isTypeOfFunction(invariantMessage)
? invariantMessage(result, ...args)
: invariantMessage
);
}
return result;
} catch (error) {
// eslint-disable-next-line no-ex-assign
error = linkError(mapError?.(error, ...args), error);
log?.error(error);
throw error;
}
}
shared.redefineName(task, name);
return Object.assign(
task,
createTaskRunner({
log
})
);
}
};
}
const rules = new Intl.PluralRules();
const similarPlurals = {
two: 'few',
few: 'many',
many: 'other',
zero: 'other'
};
const getPluralForm = (current, forms) =>
forms[current] ?? getPluralForm(similarPlurals[current], forms);
const plural = (n, forms) => getPluralForm(rules.select(n), forms).replace('%d', n.toString());
const sortingCollator = new Intl.Collator('en');
const compare = {
by: (fn, compareFn) => (a, b) => compareFn(fn(a), fn(b)),
locale: sortingCollator.compare.bind(sortingCollator)
};
async function existsVfsPath(ctx, path = '.') {
const meta = ctx.get(path);
ctx.log.debug('Check exists %s', displayPath(ctx, path));
if (isKnownDeletedPath(ctx, path)) return false;
// If we know any non-deleted descendants of this path, then it's exists
if (meta || getVfsNonDeletedDescendants(ctx, path).length > 0) return true;
return await ctx.backend.exists(path);
}
async function isVfsFile(ctx, path = '.') {
const meta = ctx.get(path);
return meta ? !meta.deleted : await ctx.backend.isFile(path);
}
async function isVfsDir(ctx, path = '.') {
return (
ctx.getRelativeChanges(path).some(meta => !meta.deleted) || (await ctx.backend.isDir(path))
);
}
const isKnownAsDir = (ctx, path) => ctx.getRelativeChanges(path).some(meta => !meta.deleted);
/**
* Returns actual children of a directory.
*/ async function readVfsDir(ctx, path = '.') {
const originalDirChildren = await ctx.backend.readDir(path);
const relativeChanges = ctx.getRelativeChanges(path);
const isNotDeleted = name => !isKnownDeletedPath(ctx, name);
const basePath = ctx.resolve(path);
const getDirentName = path => pathe.relative(basePath, ctx.resolve(path)).split(pathe.sep)[0];
const childrenFromChanges = relativeChanges
.filter(it => isNotDeleted(it.path))
.map(it => createInMemoryDirent(getDirentName(it.path), !isKnownAsDir(ctx, it.path)))
.filter(it => Boolean(it.name.replaceAll('.', '')));
const result = std
.uniqBy(
[
...originalDirChildren
.filter(it => isNotDeleted(ctx.resolve(path, it.name)))
.filter(it => !childrenFromChanges.some(dirent => dirent.name === it.name)),
...childrenFromChanges
],
entry => entry.name
)
.sort(compare.by(std.prop('name'), compare.locale));
ctx.log.debug(
'Read %s - %s (%s)',
displayPath(ctx, path),
plural(result.length, {
one: '%d member',
other: '%d members'
}),
formatList(result.map(std.prop('name')), 3)
);
return result;
}
async function tryReadVfsFile(ctx, path, encoding) {
ctx.log.debug('Read %s', displayPath(ctx, path));
if (!(await isVfsFile(ctx, path))) return null;
const content = ctx.get(path)?.content ?? (await ctx.backend.read(path));
return encoding ? content.toString(encoding) : content;
}
async function readVfsFile(ctx, path, encoding) {
const content = await tryReadVfsFile(ctx, path, encoding);
if (content === null) {
throw new Error(`"${path}" is not file (full path: ${ctx.resolve(path)})`);
}
return content;
}
async function writeVfsFile(ctx, path, content) {
ctx.log.debug('Write %s', displayPath(ctx, path));
const pathIsDir = await isVfsDir(ctx, path);
const actualContent = pathIsDir ? null : await tryReadVfsBackendFile(ctx, path);
ensureVfsPath(ctx, ctx.resolve(path));
if (actualContent && Buffer.from(content).equals(actualContent)) {
// If content is not changed, then we can just forget about this file
ctx.unregister(path);
} else {
ctx.registerPath(path, content, isKnownDeletedPath(ctx, path), pathIsDir);
}
}
async function deleteVfsPath(ctx, path) {
ctx.log.debug('Delete %s', displayPath(ctx, path));
ctx.deletePath(path);
for (const meta of ctx.getRelativeChanges(path)) {
ctx.unregister(meta.path);
}
const parentDirName = pathe.dirname(ctx.resolve(path));
if (ctx.relative(path).startsWith('..')) {
ctx.log.warn(
"You're trying to delete a file outside of the root directory, we don't support it fully"
);
} else if (!ctx.relative(parentDirName).startsWith('..'));
}
async function renameVfs(ctx, from, ...to) {
ctx.log.debug(
'Rename %s to %s',
displayPath(ctx, from),
to.map(path => displayPath(ctx, path)).join(', ')
);
if (!(await existsVfsPath(ctx, from))) {
ctx.log.debug('Path %s not exists, rename skipped', displayPath(ctx, from));
return;
}
if (await isVfsDir(ctx, from)) {
throw new Error('Renaming a directory is not supported');
}
const content = await readVfsFile(ctx, from);
await deleteVfsPath(ctx, from);
await std.concurrently(to, path => writeVfsFile(ctx, path, content), 5);
}
async function getVfsActions(ctx, types) {
const changes = await std.concurrently(
ctx.getAllDirectChanges(),
async ({ path, deleted, content, ...meta }) => {
// ctx.log.debug('Resolving required action for %s', displayPath(ctx, path));
const exists = await ctx.backend.exists(path);
if (deleted && !exists) {
return null;
}
if (deleted) {
return {
...meta,
path,
type: 'delete'
};
}
if (!content) return null;
return {
...meta,
path,
type: exists ? 'update' : 'create',
content
};
}
);
// ctx.log.debug('Found %d changes under "%s" working directory', changes.length, ctx.path);
return types
? changes.filter(action => std.isTruthy(action) && types.includes(action.type))
: std.compact(changes);
}
/** Guarantees all ancestor directories not deleted */ function ensureVfsPath(ctx, path) {
const parent = pathe.dirname(path);
if (ctx.relative(parent).startsWith('..') || parent === path) return;
if (isKnownDeletedPath(ctx, parent)) ctx.registerPath(parent, null, true);
else if (!ctx.get(parent)?.updatedAfterDelete) ctx.unregister(parent);
ensureVfsPath(ctx, parent);
}
function getVfsNonDeletedDescendants(ctx, path) {
return ctx.getRelativeChanges(path).filter(meta => !meta.deleted);
}
async function tryReadVfsBackendFile(ctx, path) {
const resolved = ctx.resolve(path);
return (await ctx.backend.isFile(resolved)) ? await ctx.backend.read(resolved) : null;
}
const prefixSize = 28;
const displayPath = (ctx, path) => {
const prefix =
ctx.path.length > prefixSize
? `${ctx.path.slice(0, prefixSize / 4)}...${ctx.path.slice((-prefixSize * 3) / 4)}`
: ctx.path;
return `${colors.colors.gray(prefix + pathe.sep)}${ctx.relative(path)}`;
};
const isDirectDeletedPath = (ctx, path) => ctx.get(path)?.deleted;
/** The directory itself or any of its ancestors is deleted */ const isKnownDeletedPath = (
ctx,
path
) => isDirectDeletedPath(ctx, path) || Boolean(hasDeletedAncestor(ctx, path) && !ctx.get(path));
const hasDeletedAncestor = (ctx, path) => {
while (!isOuterPath(ctx, (path = parentPath(ctx, path)))) {
if (isDirectDeletedPath(ctx, path) || ctx.get(path)?.updatedAfterDelete) return true;
}
return false;
};
const parentPath = (ctx, path) => ctx.resolve(pathe.join(path, '..'));
const isOuterPath = (ctx, path) => ctx.relative(path).startsWith('..');
exports.createInMemoryBackend = createInMemoryBackend;
exports.createInMemoryFilesRecord = createInMemoryFilesRecord;
exports.createTaskRunner = createTaskRunner;
exports.deleteVfsPath = deleteVfsPath;
exports.displayPath = displayPath;
exports.existsVfsPath = existsVfsPath;
exports.formatList = formatList;
exports.getVfsActions = getVfsActions;
exports.isVfsDir = isVfsDir;
exports.isVfsFile = isVfsFile;
exports.pathStartsWith = pathStartsWith;
exports.readVfsDir = readVfsDir;
exports.readVfsFile = readVfsFile;
exports.renameVfs = renameVfs;
exports.tryReadVfsFile = tryReadVfsFile;
exports.writeVfsFile = writeVfsFile;
//# sourceMappingURL=operations-C-RkTDIe.cjs.map