@nestjs/cli
Version:
Nest - modern, fast, powerful node.js web framework (@cli)
186 lines (185 loc) • 7.66 kB
JavaScript
/**
* A minimal synchronous glob over `fs` + `minimatch`, covering just the
* surface `assets-manager` needs: one pattern, one optional `ignore`
* pattern, and `dot`.
*
* This replaced the `glob` package (see nestjs/nest-cli#3520), and asset
* copying is expected to behave exactly as it did before that swap. Several
* rules below therefore look arbitrary in isolation — zero-segment trailing
* globstars, the symlink depth budget — but each one preserves a documented
* behavior that build configurations already depend on. The tests in
* `test/lib/compiler/helpers/glob.spec.ts` pin them.
*/
import { readdirSync, statSync } from 'fs';
import { minimatch } from 'minimatch';
/**
* Characters that make a path segment a pattern rather than a literal, under
* minimatch's default options (no extglob).
*
* Matches the literal chars `*`, `?`, `[`, `{` anywhere, plus `!`, `+`, `@`,
* `?`, or `*` immediately followed by `(` (an extglob-looking prefix, which
* minimatch still treats specially even without extglob enabled). Chars like
* `@`, `(`, `)`, `]`, `}` on their own are NOT magic here: treating them as
* such previously collapsed the literal base far above the real directory
* (e.g. an `@` in a username segment), causing `readdir` to walk huge trees
* and silently return no matches on the first unreadable subdirectory.
*/
const MAGIC_CHARS = /[*?[{]|[!+@?*]\(/;
const toPosix = (value) => value.replace(/\\/g, '/');
/**
* Splits a pattern into the literal prefix we can hand to `readdir` and the
* trailing segments that still need matching: `/src/a/*.hbs` -> `/src/a`.
*/
function splitPattern(pattern) {
const segments = pattern.split('/');
const magicIndex = segments.findIndex((segment) => MAGIC_CHARS.test(segment));
if (magicIndex === -1) {
return { base: pattern, rest: [] };
}
return {
base: segments.slice(0, magicIndex).join('/') || '/',
rest: segments.slice(magicIndex),
};
}
/**
* A trailing globstar matches zero segments, so `a/**` must match `a`
* itself; `minimatch` alone does not. Expanding the suffix restores that for
* the include pattern, and for `ignore` it prunes the ignored directory
* itself rather than only its contents.
*/
function expandGlobstarSuffix(pattern) {
return pattern.endsWith('/**') ? [pattern, pattern.slice(0, -3)] : [pattern];
}
/**
* How far below a symlinked directory a pattern can still reach.
*
* A globstar never recurses *through* a symlinked directory, but the
* explicit segments that follow the last one keep matching normally once
* there. So a pattern ending in a bare globstar stops at the symlink itself
* and gets a budget of 0; adding one more segment after it reaches a single
* level inside (budget 1), and two segments reach two levels (budget 2). A
* pattern with no globstar at all does no recursion of this kind, so its own
* depth is the only bound.
*
* Returning a finite number also bounds the walk: symlink cycles terminate
* because each crossing costs depth we can never regain.
*/
function symlinkDepthBudget(rest) {
for (let index = rest.length - 1; index >= 0; index--) {
if (rest[index].includes('**')) {
return rest.length - index - 1;
}
}
return rest.length;
}
/**
* Walks the directory tree rooted at `dir`, without using `fs`'s own
* `recursive` option so we can:
*
* - Skip an unreadable directory instead of failing the whole walk: one
* `EACCES` deep in an asset tree must not silently empty the result.
* - Stop descending once we are `budget` levels past a symlinked directory
* (see `symlinkDepthBudget`), which also makes symlink cycles terminate.
*
* `depth` is how many levels below the nearest symlinked ancestor an entry
* sits; 0 means it was reached without crossing one.
*/
function walk(dir, recursive, budget, depth = 0) {
let entries;
try {
entries = readdirSync(dir, { withFileTypes: true });
}
catch {
// Unreadable or missing directory: contribute nothing here, but let
// sibling directories elsewhere in the walk still report their matches.
return [];
}
const results = entries.map((entry) => ({
parent: dir,
entry,
}));
if (!recursive) {
return results;
}
for (const entry of entries) {
const full = `${dir}/${entry.name}`;
// Below a symlink every further level costs budget; above one, plain
// directory recursion is unbounded.
if (entry.isDirectory()) {
const childDepth = depth === 0 ? 0 : depth + 1;
if (childDepth <= budget) {
results.push(...walk(full, true, budget, childDepth));
}
continue;
}
// `Dirent.isDirectory()` is false for a symlink (it reports on the link
// itself), so a symlinked directory only ever reaches this branch.
if (entry.isSymbolicLink() && depth + 1 <= budget) {
let stats;
try {
stats = statSync(full);
}
catch {
continue;
}
if (stats.isDirectory()) {
results.push(...walk(full, true, budget, depth + 1));
}
}
}
return results;
}
/**
* Expands `pattern` to every matching path, with the type of each entry.
* See the file header for the compatibility rules this preserves.
*/
export function globEntriesSync(pattern, options = {}) {
const { ignore, dot = false } = options;
const normalized = toPosix(pattern);
const { base, rest } = splitPattern(normalized);
const includeMatchers = expandGlobstarSuffix(normalized);
const ignoreMatchers = ignore ? expandGlobstarSuffix(toPosix(ignore)) : [];
const accept = (candidate) => includeMatchers.some((matcher) => minimatch(candidate, matcher, { dot })) &&
!ignoreMatchers.some((matcher) => minimatch(candidate, matcher, { dot }));
// No wildcard anywhere: the pattern names one concrete path.
if (rest.length === 0) {
const stats = statSync(normalized, { throwIfNoEntry: false });
if (!stats || !accept(normalized)) {
return [];
}
return [
{
path: normalized,
isFile: stats.isFile(),
isDirectory: stats.isDirectory(),
},
];
}
// Only the immediate children of `base` can match a single trailing
// wildcard segment, so skip the deep walk in that (common) case.
const recursive = rest.length > 1 || rest.some((segment) => segment.includes('**'));
// An asset may be configured for a directory that does not exist, or one
// that turns out to be unreadable; `walk` reports no matches for it (and
// any nested directories that fail) rather than failing the whole build.
const walked = walk(base, recursive, symlinkDepthBudget(rest));
const matches = [];
// A trailing `**` matches zero segments, so `base` itself can be a result.
if (accept(base)) {
matches.push({ path: base, isFile: false, isDirectory: true });
}
for (const { parent, entry } of walked) {
const full = `${toPosix(parent)}/${entry.name}`;
if (accept(full)) {
matches.push({
path: full,
isFile: entry.isFile(),
isDirectory: entry.isDirectory(),
});
}
}
return matches;
}
/** Expands `pattern` to every matching path. */
export function globSync(pattern, options = {}) {
return globEntriesSync(pattern, options).map((entry) => entry.path);
}