Initalize

This commit is contained in:
Your Name
2026-05-03 12:12:57 -04:00
commit 38652eb9b5
10603 changed files with 1762136 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
import container from 'markdown-it-container';
export default function(name, opts = {}, md) {
return [container, name, {
render(tokens, index, _options, env) {
const token = tokens[index];
const info = token.info.trim().slice(name.length).trim() || opts.defaultTitle;
const attrs = md.renderer.renderAttrs(token);
// opening tag
if (token.nesting === 1) {
const title = info ? md.renderInline(info, {references: env.references}) : undefined;
const titleMarkdown = title ? `<p class="custom-block-title">${title ? title : ''}</p>` : '';
// special handling for details
if (name === 'details') return `<details class="${name} custom-block"${attrs}><summary>${title}</summary>\n`;
// otherwise
return `<div class="${name} custom-block"${attrs}>${titleMarkdown}\n`;
// closing tag
} else return name === 'details' ? `</details>\n` : `</div>\n`;
}},
];
};

View File

@@ -0,0 +1,101 @@
import {join, relative, posix} from 'node:path';
import {platform} from 'node:os';
import {createContentLoader as ccl} from 'vitepress';
import {nanoid} from 'nanoid';
import {parse} from 'node-html-parser';
import Debug from 'debug';
import {default as addContributors} from '../node/add-contributors.js';
import {default as addMetadata} from '../node/add-metadata.js';
import {default as augmentAuthors} from '../node/augment-authors.js';
import {default as buildCollections} from '../node/build-collections.js';
import {default as getContributors} from '../utils/get-contributors.js';
import {default as normalizeFrontmatter} from '../node/normalize-frontmatter.js';
import {default as normalizeLegacyFrontmatter} from '../node/normalize-legacy-frontmatter.js';
import {default as parseCollections} from '../node/parse-collections.js';
const windowsSlashRE = /\\/g;
const slash = p => p.replace(windowsSlashRE, '/');
const isWindows = platform() === 'win32';
const normalizePath = id => posix.normalize(isWindows ? slash(id) : id);
const getRelativePath = (url, {srcDir, cleanUrls = false} = {}) => {
return normalizePath(relative(srcDir, join(srcDir, url)))
.replace(/(^|\/)index\.html$/, '$1')
.replace(/\.html$/, cleanUrls ? '' : '.md');
};
export default function createContentLoader(patterns = [], {
siteConfig,
excerpt = false,
render = false,
} = {},
{
debug = Debug('@lando/create-content-loader'), // eslint-disable-line
} = {}) {
return ccl(patterns, {
render: true,
excerpt: true,
async transform(raw) {
const contributors = siteConfig?.userConfig?.themeConfig?.contributors ?? false;
const root = siteConfig?.userConfig?.gitRoot;
const team = contributors !== false ? await getContributors(root, contributors, {debug: debug.extend('get-contribs'), paths: []}) : [];
debug('discovered full team info %o', team);
const pages = await Promise.all(raw.map(async data => {
// backwards compute the relativePath
data.relativePath = getRelativePath(data.url, siteConfig);
// make sure siteConfig.collections exists and is populated
await buildCollections(siteConfig, {debug});
// normalize legacy frontmatter
await normalizeLegacyFrontmatter(data, {siteConfig, debug});
// normalize frontmatter
await normalizeFrontmatter(data, {siteConfig, debug});
// add contributor information
await addContributors(data, {siteConfig, debug});
// add metadata information
await addMetadata(data, {siteConfig, debug});
// parse collections
await parseCollections(data, {siteConfig, debug});
// normalize authors
await augmentAuthors(data, {team, debug});
// get stuff
const {frontmatter, html, url} = data;
// ensure we have a title
if (!frontmatter.title) frontmatter.title = parse(html).getElementsByTagName('h1')[0]?.text ?? frontmatter.title;
// munge it all 2getha
const content = Object.assign(frontmatter, {
id: nanoid(),
title: frontmatter.title,
summary: frontmatter.summary ?? frontmatter.byline ?? frontmatter.description,
authors: frontmatter.authors,
date: frontmatter.date ?? data.timestamp ?? data.datetime,
datetime: data.datetime,
excerpt: excerpt ? data.excerpt : '',
html: render ? data.html : '',
relativePath: data.relativePath,
tags: frontmatter.tags ?? [],
timestamp: data.timestamp ?? Date.now(),
type: frontmatter.collection,
url,
});
// remove some stuff we do not need
// @TODO: any other optimization here?
delete content.head;
// return
return content;
}));
// sort and return
return pages.sort((a, b) => b.timestamp - a.timestamp);
},
});
};

View File

@@ -0,0 +1,52 @@
import {spawn} from 'node:child_process';
import {merge} from 'lodash-es';
import {bold, dim} from 'colorette';
import {default as mergePromise} from './merge-promise.js';
import Debug from 'debug';
export default function(defaults = {}) {
return function(command, args = [], options = {}, stdout = '', stderr = '') {
// @TODO: error handling?
// merge our options over the defaults
options = merge({debug: Debug('@lando/run-command'), ignoreReturnCode: false, env: process.env}, defaults, options); // eslint-disable-line
const {debug} = options;
// birth
debug('running command %o %o from %o', command, args, options?.cwd ?? process.cwd());
const child = spawn(command, args, options);
return mergePromise(child, async () => {
return new Promise((resolve, reject) => {
child.on('error', error => {
debug('command %o error %o', command, error?.message);
stderr += error?.message ?? error;
});
child.stdout.on('data', data => {
debug('%s %s', bold('stdout'), dim(data.toString().trim()));
stdout += data;
});
child.stderr.on('data', data => {
debug('%s %s', bold('stderr'), dim(data.toString().trim()));
stderr += data;
});
child.on('close', code => {
debug('command %o done with code %o', command, code);
// if code is non-zero and we arent ignoring then reject here
if (code !== 0 && !options.ignoreReturnCode) {
const error = new Error(stderr);
error.code = code;
reject(error);
}
// otherwise return
resolve({stdout, stderr, code});
});
});
});
};
};

View File

@@ -0,0 +1,9 @@
export default function detectRuntime() {
// bun
if (typeof Bun !== 'undefined' || process.versions.bun) {
return 'bun';
// node
} else return 'node';
};

View File

@@ -0,0 +1,5 @@
export default function encodeTag(data) {
if (typeof data !== 'string') return '';
return data.replaceAll(' ', '-').toLowerCase();
};

View File

@@ -0,0 +1,17 @@
// @TODO: support other platforms besides netlify?
export default function(landoPlugin) {
// if VPL_BASE_URL is set then use that
if (process.env?.VPL_BASE_URL) return process.env?.VPL_BASE_URL;
// otherwise we can try other stuff if we are on something like netlify
if (process.env?.NETLIFY && process.env.CONTEXT === 'production' && landoPlugin) return 'https://docs.lando.dev';
if (process.env?.NETLIFY && process.env.CONTEXT === 'production') return process.env.URL;
if (process.env?.NETLIFY && process.env.CONTEXT !== 'production') return process.env.DEPLOY_PRIME_URL;
// if we get here and its a lando plugin we can safely assume https://docs.lando.dev, this is mostly for github actions testing
if (landoPlugin) return 'https://docs.lando.dev';
// return nothing
return undefined;
};

View File

@@ -0,0 +1,20 @@
import {default as getStdOut} from './parse-stdout.js';
export default function async(cwd = process.cwd()) {
// lando build env directly
if (process.env?.VPL_MVB_BRANCH) return process.env?.VPL_MVB_BRANCH;
// lando build env directly
else if (process.env?.LANDO_MVB_BRANCH) return process.env?.LANDO_MVB_BRANCH;
// or from source
else if (process.env?.VPL_MVB_SOURCE) return getStdOut('git rev-parse --abbrev-ref HEAD', {cwd: process.env?.VPL_MVB_SOURCE, trim: true});
// or from source
else if (process.env?.LANDO_MVB_SOURCE) return getStdOut('git rev-parse --abbrev-ref HEAD', {cwd: process.env?.LANDO_MVB_SOURCE, trim: true});
// or if we are on netlify
else if (process.env?.NETLIFY) return process.env.HEAD;
// or GHA PR
else if (process.env?.GITHUB_HEAD_REF) return process.env.GITHUB_HEAD_REF;
// or GHA branch
else if (process.env?.GITHUB_REF_NAME) return process.env.GITHUB_REF_NAME;
// otherwise try to get it from git
else return getStdOut('git rev-parse --abbrev-ref HEAD', {cwd, trim: true});
};

View File

@@ -0,0 +1,138 @@
import {default as execSync} from './parse-stdout.js';
import findIndex from 'lodash-es/findIndex.js';
import gravatarUrl from 'gravatar-url';
import groupBy from 'lodash-es/groupBy.js';
import Debug from 'debug';
const parseStringInclude = data => {
const parts = data.trim().split(' ');
// add a single commit if we dont have any commits
if (!Number.isInteger(parseInt(parts[0]))) parts[0] = 1;
// mod part 0 so it is parsed correclty downstream
parts[0] = parts[0] = ` ${parts[0]}\t`;
return parts.join(' ');
};
export default function async(
cwd,
{
merge = 'name',
debotify = true,
include = [],
exclude = [],
} = {},
{
debug = Debug('@lando/get-contributors'), // eslint-disable-line
paths = [],
} = {},
) {
// start with a command that will get ALL THE AUTHORS
const command = ['git', '--no-pager', 'shortlog', '-nes', 'HEAD'];
const opts = {cwd, stdin: 'inherit'};
// then scope to paths if appropriate
if (paths.length > 0) command.push('--', ...paths);
// run
debug('running command %o with exec options %o', command, opts);
const stdout = execSync(command.join(' '), opts);
// parse git data into a string
let data = stdout.split('\n');
// separate out include strings and objects
const includeStrings = include.filter(contributor => typeof contributor === 'string') ?? [];
const includeObjects = include.filter(contributor => typeof contributor === 'object') ?? [];
// add in any include strings
if (includeStrings.length > 0) for (const contributor of includeStrings) data.push(parseStringInclude(contributor));
// map strings to <VPTeamMembersItem.vue> compatible objects
data = data.map(item => item.trim().match(/^(\d+)\t(.*) <(.*)>$/))
.filter(item => item !== null)
.map(([, commits, name, email]) => ({
commits: Number.parseInt(commits, 10),
email,
name: name.trim(),
avatar: gravatarUrl(email),
title: undefined,
org: undefined,
maintainer: false,
links: [],
}));
// add in any include objects
if (includeObjects.length > 0) {
for (const contributor of includeObjects) {
// try to see if we already have this contrib
const existing = data.find(member => member.email === contributor.email || member.email === contributor.mergeWith);
// if we do then update it
if (existing) Object.assign(existing, contributor);
// otherwise treat it as a new contrib only merge only is true
else if (!existing && contributor.mergeOnly !== true) data.push({name: '', email: '', ...contributor});
}
};
// remove any bots
if (debotify) {
data = data
.filter(contributor => !contributor.email.includes('[bot]') && !contributor.name.includes('[bot]'))
.filter(contributor => contributor.email !== 'rtfm47@lando.dev');
}
// remove any excluded contributors
if (exclude.length > 0) {
for (let excluded of exclude) {
// if excluded is a string then map into an object
if (typeof excluded === 'string' && excluded.match(/^(.*) <(.*)>$/) !== null) {
const parts = excluded.match(/^(.*) <(.*)>$/);
excluded = {name: parts[1], email: parts[2]};
}
// attampte to exclude
if (findIndex(data, excluded) > -1) data.splice(findIndex(data, excluded), 1);
}
}
// attempt to merge same named entries together
// this will prefer the member metadata eg email, avatar etc with the most commits
// it will also add all the commits together
if (merge !== false && ['email', 'name'].includes(merge)) {
const grouped = groupBy(data, merge);
// attempt merge strategy for any merge with more than one match
for (const [id, matches] of Object.entries(grouped)) {
if (matches.length > 1) {
const best = matches[0];
best.commits = matches.map(match => match.commits).reduce((sum, amount) => sum + amount, 0);
// special handling for org/title
for (const special of ['links', 'org', 'title']) {
if (!best[special] || (Array.isArray(best[special]) && best[special].length === 0)) {
best[special] = matches
.map(match => match[special])
.filter(data => data !== undefined && data !== null && data !== '' && data.length !== 0)[0];
}
}
// reset matches
grouped[id] = [best];
}
}
// reset data with merged things
data = Object.entries(grouped).map(([name, matches]) => matches[0]);
}
// sort by commits
data = data.sort((a, b) => b.commits - a.commits);
// separate maintainers from contribs
const maintainers = data.filter(contrib => contrib.maintainer);
const contributors = data.filter(contrib => !contrib.maintainer);
// return contribs with maintainers in the front
return maintainers.concat(contributors);
}

View File

@@ -0,0 +1,12 @@
export default function(id) {
return [
['script', {async: true, src: `https://www.googletagmanager.com/gtag/js?id=${id}`}],
['script', {}, [
'window.dataLayer = window.dataLayer || [];',
'function gtag(){dataLayer.push(arguments);}',
`gtag('js', new Date());`,
`gtag('config', '${id}');`,
].join('\n'),
],
];
};

View File

@@ -0,0 +1,18 @@
export default function(id) {
return [
['script', {
async: true,
defer: true,
id: 'hs-script-loader',
src: `//js.hs-scripts.com/${id}.js`,
}],
['script', {}, [
'window.dataLayer = window.dataLayer || [];',
'window.hubspot = function(){dataLayer.push(arguments);}',
`hubspot('js', new Date());`,
`hubspot('config', '${id}');`,
].join('\n'),
],
];
};

View File

@@ -0,0 +1,12 @@
import {default as normalize} from './normalize-2base.js';
export default function getItemNormalizedLink(item, site) {
// if we dont have what we need just return that garbage
if (!item.link) return item.link;
// if this is not a special mvb then just return
if (item.rel !== 'mvb') return item.link;
// otherwise normalize on version base
return normalize(item.link, site?.themeConfig?.multiVersionBuild?.base ?? '/', site);
};

View File

@@ -0,0 +1,84 @@
import * as semver from 'es-semver';
import {default as getBranch} from './get-branch.js';
import {default as getStdOut} from './parse-stdout.js';
import Debug from 'debug';
export default function async(
cwd,
{
match = 'v[0-9].*',
satisfies = '*',
} = {},
{
debug = Debug('@lando/get-tags'), // eslint-disable-line
} = {},
) {
// stdout opts
const opts = {cwd, trim: true};
// quiet opts
const qopts = {...opts, stdio: ['pipe', 'pipe', 'ignore']};
// commands
const tagCmd = ['git', '--no-pager', 'tag', '--list', `"${match}"`];
const devReleaseCmd = ['git', 'describe', '--tags', '--always', '--abbrev=1', `--match="${match}"`];
debug('getting tags with %o with exec options %o', tagCmd, opts);
debug('getting dev release with %o with exec options %o', devReleaseCmd, opts);
const tags = getStdOut(tagCmd.join(' '), opts);
debug('matched %o tags with %o', tags.split('\n').length, match);
// match tags to versions
const versions = semver.rsort(tags.split('\n')
.filter(tag => typeof tag === 'string')
.filter(tag => semver.valid(semver.clean(tag)) !== null)
.filter(tag => semver.satisfies(semver.clean(tag), satisfies, {includePrerelease: true}) === true));
debug('matched %o versions using %o', versions.length, satisfies);
// set aliases to HEAD by default
const aliases = {dev: 'HEAD', edge: 'HEAD', stable: 'HEAD'};
// get the dev alias
aliases.dev =
process?.env?.VPL_MVB_DEV_VERSION ??
getStdOut(`${devReleaseCmd.join(' ')} ${getBranch(cwd)} || ${devReleaseCmd.join(' ')}`, qopts);
// if we have versions data we can reset them to actual tags
if (versions.length > 0) {
aliases.edge = versions[0];
aliases.stable = versions.filter(version => semver.prerelease(version) === null)[0];
}
debug('generated aliases %o', aliases);
// construct extended information for ALL versions
const extended = versions.map(version => ({
ref: version,
semantic: semver.clean(version),
version: version,
}));
// add build aliases into extended unless the alias does not exist yet or is invalid
for (const [alias, ref] of Object.entries(aliases)) {
if (semver.valid(ref) !== null && alias !== 'dev') {
extended.push({
alias,
ref,
semantic: semver.clean(ref),
version: ref,
});
}
}
// dev should always exist in extended
extended.push({
alias: 'dev',
ref: getBranch(cwd),
semantic: semver.valid(aliases.dev) === null ? '0.0.0' : semver.clean(aliases.dev),
version: semver.valid(aliases.dev) === null ? 'v0.0.0' : aliases.dev,
});
debug('generated extended info %o', extended);
return {aliases, extended, versions};
}

View File

@@ -0,0 +1,28 @@
import {existsSync} from 'node:fs';
import {basename, dirname} from 'node:path';
import Debug from 'debug';
import {default as execSync} from './parse-stdout.js';
export default function async(file,
{
debug = Debug('@lando/get-timestamp'), // eslint-disable-line
} = {},
) {
// blow up
const cwd = dirname(file);
const fileName = basename(file);
// if this is a new file then i guess just return now?
if (!existsSync(cwd)) return Date.now();
// command and opts
const command = ['git', 'log', '-1', '--pretty="%ai"', fileName];
const opts = {cwd, stdin: 'inherit'};
// run
debug('running command %o with exec options %o', command, opts);
const stdout = execSync(command.join(' '), opts);
return stdout.trim();
}

View File

@@ -0,0 +1,19 @@
const EXT_RE = /(index)?\.(md|html)$/;
const HASH_RE = /#.*$/;
const inBrowser = typeof document !== 'undefined';
const normalize = path => decodeURI(path).replace(HASH_RE, '').replace(EXT_RE, '');
export default function isActive(currentPath, matchPath, asRegex) {
if (matchPath === undefined) return false;
currentPath = normalize(`/${currentPath}`);
if (asRegex) return new RegExp(matchPath).test(currentPath);
if (normalize(matchPath) !== currentPath) return false;
const hashMatch = matchPath.match(HASH_RE);
if (hashMatch) return (inBrowser ? location.hash : '') === hashMatch[0];
return true;
};

View File

@@ -0,0 +1,22 @@
import * as semver from 'es-semver';
export default function(version) {
// throw error if not a valid version
if (semver.valid(semver.clean(version)) === null) {
throw new Error(`${version} must be a semantic version for this to work!`);
}
// parse the version
version = semver.parse(version);
// if prerelease is empty then this is stable version
if (version.prerelease.length === 0) return false;
// if prerelease is length 2 with string and integer parts then this is a non-dev prerelease
if (version.prerelease.length === 2 && typeof version.prerelease[0] === 'string' && Number.isInteger(version.prerelease[1])) {
return false;
}
// if we get here then lets just assume its a dev release?
return true;
}

View File

@@ -0,0 +1,12 @@
export default function(path, domains = []) {
// filter out non-strings
domains = domains.filter(domain => typeof domain === 'string');
// separate out strings and regex
const regexers = domains.map(domain => new RegExp(domain)) ?? [];
return false
|| domains.find(domain => path.startsWith(domain)) !== undefined
|| regexers.map(regexer => regexer.test(path)).find(matched => matched === true) !== undefined;
}

View File

@@ -0,0 +1,16 @@
const nativePromisePrototype = (async () => {})().constructor.prototype;
const descriptors = ['then', 'catch', 'finally']
.map(property => [
property,
Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property),
]);
export default function(thing, promise) {
for (const [property, descriptor] of descriptors) {
// eslint-disable-next-line max-len
const value = typeof promise === 'function' ? (...args) => Reflect.apply(descriptor.value, promise(), args) : descriptor.value.bind(promise);
Reflect.defineProperty(thing, property, {...descriptor, value});
}
return thing;
};

View File

@@ -0,0 +1,50 @@
const EXTERNAL_URL_RE = /^(?:[a-z]+:|\/\/)/i;
const KNOWN_EXTENSIONS = new Set();
const isExternal = path => EXTERNAL_URL_RE.test(path);
const joinPath = (base, path) => `${base}${path}`.replace(/\/+/g, '/');
const treatAsHtml = filename => {
if (KNOWN_EXTENSIONS.size === 0) {
const extraExts =
(typeof process === 'object' && process.env?.VITE_EXTRA_EXTENSIONS) ||
(import.meta).env?.VITE_EXTRA_EXTENSIONS ||
'';
// md, html? are intentionally omitted
;(
'3g2,3gp,aac,ai,apng,au,avif,bin,bmp,cer,class,conf,crl,css,csv,dll,' +
'doc,eps,epub,exe,gif,gz,ics,ief,jar,jpe,jpeg,jpg,js,json,jsonld,m4a,' +
'man,mid,midi,mjs,mov,mp2,mp3,mp4,mpe,mpeg,mpg,mpp,oga,ogg,ogv,ogx,' +
'opus,otf,p10,p7c,p7m,p7s,pdf,png,ps,qt,roff,rtf,rtx,ser,svg,t,tif,' +
'tiff,tr,ts,tsv,ttf,txt,vtt,wav,weba,webm,webp,woff,woff2,xhtml,xml,' +
'yaml,yml,zip' +
(extraExts && typeof extraExts === 'string' ? ',' + extraExts : '')
).split(',').forEach(ext => KNOWN_EXTENSIONS.add(ext));
}
const ext = filename.split('.').pop();
return ext == null || !KNOWN_EXTENSIONS.has(ext.toLowerCase());
};
export default function normalize2Base(url, base = '/', site) {
const {pathname, search, hash, protocol} = new URL(url, 'http://lando.dev');
// return external urls
if (isExternal(url) || url.startsWith('#') || !protocol.startsWith('http') || !treatAsHtml(pathname)) return url;
// otherwise do the usual normalization
const path =
pathname.endsWith('/') || pathname.endsWith('.html')
? url
: url.replace(
/(?:(^\.+)\/)?.*$/,
`$1${pathname.replace(
/(\.md)?$/,
site.cleanUrls ? '' : '.html',
)}${search}${hash}`,
);
return EXTERNAL_URL_RE.test(path) || !path.startsWith('/') ? path : joinPath(base, path);
};

View File

@@ -0,0 +1,9 @@
import {default as getItemNormalizedLink} from './get-item-nl.js';
export default function normalizeItems(items, site) {
return items.map(item => {
if (item.items && Array.isArray(item.items)) return normalizeItems(item.items, site);
else if (item.rel !== 'mvb') return item;
else return {...item, link: getItemNormalizedLink(item, site)};
});
};

View File

@@ -0,0 +1,19 @@
export default function({
base = '/v/',
build = 'stable',
cache = true,
match = 'v[0-9].*',
mvbase = undefined,
satisfies = '*',
siteBase = '/',
} = {}) {
// if no mvbase then combine it with sitebase as usual, this is probabyl base reality
if (!mvbase) mvbase = `/${siteBase}/${base}/`.replace(/\/{2,}/g, '/');
// if we are in a MVB then the OG base can get lost so rebase on that
if (process.env.VPL_MVB_BUILD) mvbase = `/${process.env.VPL_MVB_BASE}/${base}/`.replace(/\/{2,}/g, '/');
if (process.env.LANDO_MVB_BUILD) mvbase = `/${process.env.LANDO_MVB_BASE}/${base}/`.replace(/\/{2,}/g, '/');
// return
return {base, build, cache, match, mvbase, satisfies};
};

View File

@@ -0,0 +1,5 @@
import {default as normalize} from './normalize-2base.js';
export default function normalizemvbLink(url, site) {
return normalize(url, site?.themeConfig?.multiVersionBuild?.mvbase ?? '/', site);
};

View File

@@ -0,0 +1,5 @@
import {default as normalize} from './normalize-2base.js';
export default function normalizerootLink(url, site) {
return normalize(url, '/', site);
};

View File

@@ -0,0 +1,16 @@
import {resolve, basename} from 'node:path';
export default function(layouts = {}) {
return Object.entries(layouts)
.map(([name, layout]) => ({
name,
var: basename(layout, '.vue'),
from: layout,
}))
.map((layout, index) => ({
...layout,
add: ` app.component('${layout.name}', ${layout.var});`,
index,
import: `import ${layout.var} from '${resolve(layout.from)}';`,
}));
}

View File

@@ -0,0 +1,6 @@
import {execSync} from 'node:child_process';
export default function(cmd, options = {}) {
const stdout = execSync(cmd, {maxBuffer: 1024 * 1024 * 10, encoding: 'utf-8', ...options});
return options.trim === true ? stdout.trim() : stdout;
};

View File

@@ -0,0 +1,5 @@
import {join} from 'node:path';
export default function(page, base, extras = []) {
return [page, ...extras.map(item => join(page, '..', item))].map(page => join(base, page));
};

View File

@@ -0,0 +1,12 @@
import {basename, join, sep} from 'node:path';
import dropRight from 'lodash-es/dropRight.js';
import range from 'lodash-es/range.js';
export default function(files, startsFrom) {
return range(startsFrom.split(sep).length)
.map(end => dropRight(startsFrom.split(sep), end).join(sep))
.map(dir => files.map(file => join(dir, basename(file))))
.flat(Number.POSITIVE_INFINITY);
};