astro-ghostcms/.pnpm-store/v3/files/4e/e246f407f30151206ea122c1f39...

297 lines
8.5 KiB
Plaintext

import path from 'node:path';
import { promises as fs } from 'node:fs';
const POSIX_SEP_RE = new RegExp('\\' + path.posix.sep, 'g');
const NATIVE_SEP_RE = new RegExp('\\' + path.sep, 'g');
/** @type {Map<string,RegExp>}*/
const PATTERN_REGEX_CACHE = new Map();
const GLOB_ALL_PATTERN = `**/*`;
const TS_EXTENSIONS = ['.ts', '.tsx', '.mts', '.cts'];
const JS_EXTENSIONS = ['.js', '.jsx', '.mjs', '.cjs'];
const TSJS_EXTENSIONS = TS_EXTENSIONS.concat(JS_EXTENSIONS);
const TS_EXTENSIONS_RE_GROUP = `\\.(?:${TS_EXTENSIONS.map((ext) => ext.substring(1)).join('|')})`;
const TSJS_EXTENSIONS_RE_GROUP = `\\.(?:${TSJS_EXTENSIONS.map((ext) => ext.substring(1)).join(
'|'
)})`;
const IS_POSIX = path.posix.sep === path.sep;
/**
* @template T
* @returns {{resolve:(result:T)=>void, reject:(error:any)=>void, promise: Promise<T>}}
*/
export function makePromise() {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
return { promise, resolve, reject };
}
/**
* loads typescript async to avoid direct dependency
* @returns {Promise<any>}
*/
export async function loadTS() {
try {
return import('typescript').then((m) => m.default);
} catch (e) {
console.error('typescript must be installed to use "native" functions');
throw e;
}
}
/**
* @param {string} filename
* @param {import('./cache.js').TSConfckCache} [cache]
* @returns {Promise<string|void>}
*/
export async function resolveTSConfigJson(filename, cache) {
if (path.extname(filename) !== '.json') {
return; // ignore files that are not json
}
const tsconfig = path.resolve(filename);
if (cache && (cache.hasParseResult(tsconfig) || cache.hasParseResult(filename))) {
return tsconfig;
}
return fs.stat(tsconfig).then((stat) => {
if (stat.isFile() || stat.isFIFO()) {
return tsconfig;
} else {
throw new Error(`${filename} exists but is not a regular file.`);
}
});
}
/**
*
* @param {string} dir an absolute directory path
* @returns {boolean} if dir path includes a node_modules segment
*/
export const isInNodeModules = IS_POSIX
? (dir) => dir.includes('/node_modules/')
: (dir) => dir.match(/[/\\]node_modules[/\\]/);
/**
* convert posix separator to native separator
*
* eg.
* windows: C:/foo/bar -> c:\foo\bar
* linux: /foo/bar -> /foo/bar
*
* @param {string} filename with posix separators
* @returns {string} filename with native separators
*/
export const posix2native = IS_POSIX
? (filename) => filename
: (filename) => filename.replace(POSIX_SEP_RE, path.sep);
/**
* convert native separator to posix separator
*
* eg.
* windows: C:\foo\bar -> c:/foo/bar
* linux: /foo/bar -> /foo/bar
*
* @param {string} filename - filename with native separators
* @returns {string} filename with posix separators
*/
export const native2posix = IS_POSIX
? (filename) => filename
: (filename) => filename.replace(NATIVE_SEP_RE, path.posix.sep);
/**
* converts params to native separator, resolves path and converts native back to posix
*
* needed on windows to handle posix paths in tsconfig
*
* @param dir {string|null} directory to resolve from
* @param filename {string} filename or pattern to resolve
* @returns string
*/
export const resolve2posix = IS_POSIX
? (dir, filename) => (dir ? path.resolve(dir, filename) : path.resolve(filename))
: (dir, filename) =>
native2posix(
dir
? path.resolve(posix2native(dir), posix2native(filename))
: path.resolve(posix2native(filename))
);
/**
*
* @param {import('./public.d.ts').TSConfckParseResult} result
* @param {import('./public.d.ts').TSConfckParseOptions} [options]
* @returns {string[]}
*/
export function resolveReferencedTSConfigFiles(result, options) {
const dir = path.dirname(result.tsconfigFile);
return result.tsconfig.references.map((ref) => {
const refPath = ref.path.endsWith('.json')
? ref.path
: path.join(ref.path, options?.configName ?? 'tsconfig.json');
return resolve2posix(dir, refPath);
});
}
/**
* @param {string} filename
* @param {import('./public.d.ts').TSConfckParseResult} result
* @returns {import('./public.d.ts').TSConfckParseResult}
*/
export function resolveSolutionTSConfig(filename, result) {
const allowJs = result.tsconfig.compilerOptions?.allowJs;
const extensions = allowJs ? TSJS_EXTENSIONS : TS_EXTENSIONS;
if (
result.referenced &&
extensions.some((ext) => filename.endsWith(ext)) &&
!isIncluded(filename, result)
) {
const solutionTSConfig = result.referenced.find((referenced) =>
isIncluded(filename, referenced)
);
if (solutionTSConfig) {
return solutionTSConfig;
}
}
return result;
}
/**
*
* @param {string} filename
* @param {import('./public.d.ts').TSConfckParseResult} result
* @returns {boolean}
*/
function isIncluded(filename, result) {
const dir = native2posix(path.dirname(result.tsconfigFile));
const files = (result.tsconfig.files || []).map((file) => resolve2posix(dir, file));
const absoluteFilename = resolve2posix(null, filename);
if (files.includes(filename)) {
return true;
}
const allowJs = result.tsconfig.compilerOptions?.allowJs;
const isIncluded = isGlobMatch(
absoluteFilename,
dir,
result.tsconfig.include || (result.tsconfig.files ? [] : [GLOB_ALL_PATTERN]),
allowJs
);
if (isIncluded) {
const isExcluded = isGlobMatch(absoluteFilename, dir, result.tsconfig.exclude || [], allowJs);
return !isExcluded;
}
return false;
}
/**
* test filenames agains glob patterns in tsconfig
*
* @param filename {string} posix style abolute path to filename to test
* @param dir {string} posix style absolute path to directory of tsconfig containing patterns
* @param patterns {string[]} glob patterns to match against
* @param allowJs {boolean} allowJs setting in tsconfig to include js extensions in checks
* @returns {boolean} true when at least one pattern matches filename
*/
export function isGlobMatch(filename, dir, patterns, allowJs) {
const extensions = allowJs ? TSJS_EXTENSIONS : TS_EXTENSIONS;
return patterns.some((pattern) => {
// filename must end with part of pattern that comes after last wildcard
let lastWildcardIndex = pattern.length;
let hasWildcard = false;
for (let i = pattern.length - 1; i > -1; i--) {
if (pattern[i] === '*' || pattern[i] === '?') {
lastWildcardIndex = i;
hasWildcard = true;
break;
}
}
// if pattern does not end with wildcard, filename must end with pattern after last wildcard
if (
lastWildcardIndex < pattern.length - 1 &&
!filename.endsWith(pattern.slice(lastWildcardIndex + 1))
) {
return false;
}
// if pattern ends with *, filename must end with a default extension
if (pattern.endsWith('*') && !extensions.some((ext) => filename.endsWith(ext))) {
return false;
}
// for **/* , filename must start with the dir
if (pattern === GLOB_ALL_PATTERN) {
return filename.startsWith(`${dir}/`);
}
const resolvedPattern = resolve2posix(dir, pattern);
// filename must start with part of pattern that comes before first wildcard
let firstWildcardIndex = -1;
for (let i = 0; i < resolvedPattern.length; i++) {
if (resolvedPattern[i] === '*' || resolvedPattern[i] === '?') {
firstWildcardIndex = i;
hasWildcard = true;
break;
}
}
if (
firstWildcardIndex > 1 &&
!filename.startsWith(resolvedPattern.slice(0, firstWildcardIndex - 1))
) {
return false;
}
// if no wildcard in pattern, filename must be equal to resolved pattern
if (!hasWildcard) {
return filename === resolvedPattern;
}
// complex pattern, use regex to check it
if (PATTERN_REGEX_CACHE.has(resolvedPattern)) {
return PATTERN_REGEX_CACHE.get(resolvedPattern).test(filename);
}
const regex = pattern2regex(resolvedPattern, allowJs);
PATTERN_REGEX_CACHE.set(resolvedPattern, regex);
return regex.test(filename);
});
}
/**
* @param {string} resolvedPattern
* @param {boolean} allowJs
* @returns {RegExp}
*/
function pattern2regex(resolvedPattern, allowJs) {
let regexStr = '^';
for (let i = 0; i < resolvedPattern.length; i++) {
const char = resolvedPattern[i];
if (char === '?') {
regexStr += '[^\\/]';
continue;
}
if (char === '*') {
if (resolvedPattern[i + 1] === '*' && resolvedPattern[i + 2] === '/') {
i += 2;
regexStr += '(?:[^\\/]*\\/)*'; // zero or more path segments
continue;
}
regexStr += '[^\\/]*';
continue;
}
if ('/.+^${}()|[]\\'.includes(char)) {
regexStr += `\\`;
}
regexStr += char;
}
// add known file endings if pattern ends on *
if (resolvedPattern.endsWith('*')) {
regexStr += allowJs ? TSJS_EXTENSIONS_RE_GROUP : TS_EXTENSIONS_RE_GROUP;
}
regexStr += '$';
return new RegExp(regexStr);
}