UNPKG

@plugjs/plug

Version:
229 lines (198 loc) 8.15 kB
import { statSync } from 'node:fs' import { createRequire } from 'node:module' import { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { assert } from './asserts' /** A _branded_ `string` representing an _absolute_ path name */ export type AbsolutePath = string & { __brand_absolute_path: never } /* ========================================================================== * * PATH FUNCTIONS * * ========================================================================== */ /** Resolve a path into an {@link AbsolutePath} */ export function resolveAbsolutePath(directory: AbsolutePath, ...paths: string[]): AbsolutePath { assertAbsolutePath(directory) return resolve(directory, ...paths) as AbsolutePath } /** * Resolve a path as a relative path to the directory specified, returning the * relative child path or `undefined` if the specified path was not a child. * * The `path` specified here _could_ also be another {@link AbsolutePath} * therefore something like this will work: * * ``` * resolveRelativeChildPath('/foo', '/foo/bar') * // will yield `bar` * ``` */ export function resolveRelativeChildPath(directory: AbsolutePath, ...paths: string[]): string | undefined { const abs = resolveAbsolutePath(directory, ...paths) const rel = relative(directory, abs) return (isAbsolute(rel) || (rel === '..') || rel.startsWith(`..${sep}`)) ? undefined : rel } /** * Asserts that a path is a relative path to the directory specified, failing * the build if it's not (see also {@link resolveRelativeChildPath}). */ export function assertRelativeChildPath(directory: AbsolutePath, ...paths: string[]): string { const relative = resolveRelativeChildPath(directory, ...paths) assert(relative, `Path "${join(...paths)}" not relative to "${directory}"`) return relative } /** Checks that the specified path is an {@link AbsolutePath} */ export function isAbsolutePath(path: string): path is AbsolutePath { return isAbsolute(path) } /** Asserts that the specified path is an {@link AbsolutePath} */ export function assertAbsolutePath(p: string): asserts p is AbsolutePath { assert(isAbsolute(p), `Path "${p}" not absolute`) } /** Return the {@link AbsolutePath} parent of another */ export function getAbsoluteParent(path: AbsolutePath): AbsolutePath { assertAbsolutePath(path) return dirname(path) as AbsolutePath } /** * Return the {@link process.cwd() | current working directory} as an * {@link AbsolutePath}. */ export function getCurrentWorkingDirectory(): AbsolutePath { const cwd = process.cwd() assertAbsolutePath(cwd) return cwd } /** * Return the _common_ path amongst all specified paths. * * While the first `path` _must_ be an {@link AbsolutePath}, all other `paths` * can be _relative_ and will be resolved against the first `path`. */ export function commonPath(path: AbsolutePath, ...paths: string[]): AbsolutePath { assertAbsolutePath(path) // Here the first path will be split into its components // on win => [ 'C:', 'Windows', 'System32' ] // on unx => [ '', 'usr' const components = normalize(path).split(sep) let length = components.length for (const current of paths) { const absolute = resolveAbsolutePath(path, current) const parts = absolute.split(sep) for (let i = 0; i < length; i++) { if (components[i] !== parts[i]) { length = i break } } assert(length, 'No common ancestors amongst paths') } const common = components.slice(0, length).join(sep) assertAbsolutePath(common) return common } /* ========================================================================== * * MODULE RESOLUTION FUNCTIONS * * ========================================================================== */ function resolveFilename(__fileurl: string): AbsolutePath { const file = __fileurl.startsWith('file:') ? fileURLToPath(__fileurl) : __fileurl assertAbsolutePath(file) return file } /** Return the equivalent of `__filename` from our `__fileurl` pseudo variable */ export function filenameFromUrl(__fileurl: string): AbsolutePath { const file = resolveFilename(__fileurl) assert(resolveFile(file), `Unable to resolve "${__fileurl}" as a file`) return file } /** Return the equivalent of `__dirname` from our `__fileurl` pseudo variable */ export function dirnameFromUrl(__fileurl: string): AbsolutePath { const dir = getAbsoluteParent(resolveFilename(__fileurl)) assert(resolveDirectory(dir), `Unable to resolve "${__fileurl}" as a directory`) return dir } /** * Return the absolute path of a file relative to the given `__fileurl`, where * `__fileurl` is either CommonJS's own `__filename` variable, or EcmaScript's * `import.meta.url` (so either an absolute path name, or a `file:///...` url). * * If further `paths` are specified, those will be resolved as relative paths * to the original `__fileurl` so we can easily write something like this: * * ``` * const dataFile = requireFilename(__fileurl, 'data.json') * // if we write this in "/foo/bar/baz.(ts|js|cjs|mjs)" * // `dataFile` will now be "/foo/bar/data.json" * ``` */ export function requireFilename(__fileurl: string, ...paths: string[]): AbsolutePath { const file = resolveFilename(__fileurl) assertAbsolutePath(file) /* No paths? Return the file! */ if (! paths.length) return file /* Resolve any paths, relative to the file */ const directory = getAbsoluteParent(file) return resolveAbsolutePath(directory, ...paths) } /** * Return the absolute path of a file which can be _required_ or _imported_ * by Node (or forked to via `child_process.fork`). * * This leverages {@link requireFilename} to figure out the starting point where * to look for files, and will _try_ to match the same extension of `__fileurl` * (so, `.ts` for `ts-node`, `.mjs` for ESM modules, ...). */ export function requireResolve(__fileurl: string, module: string): AbsolutePath { const file = resolveFilename(__fileurl) // We do our custom resolution _only_ for local (./foo.bar) files... if (module.match(/^\.\.?\//)) { // If we import "../foo.ext" from "/a/b/c/bar.ts" we need to check: // * /a/b/foo.ext // * /a/b/foo.ext.ts // * /a/b/foo.ext/index.ts // ... then delegate to the standard "require.resolve(...)" const url = pathToFileURL(file) const ext = extname(file) const checks = [ `${module}`, `${module}${ext}`, `${module}/index${ext}` ] for (const check of checks) { const resolved = fileURLToPath(new URL(check, url)) as AbsolutePath if (resolveFile(resolved)) { module = check break } } } const require = createRequire(file) const required = require.resolve(module) assertAbsolutePath(required) return required } /* ========================================================================== * * FILE CHECKING FUNCTIONS * * ========================================================================== */ /** * Resolves the specified path as an {@link AbsolutePath} and checks it is a * _file_, returning `undefined` if it is not. */ export function resolveFile(path: AbsolutePath, ...paths: string[]): AbsolutePath | undefined { const file = resolveAbsolutePath(path, ...paths) try { const stat = statSync(file) if (stat.isFile()) return file } catch (error: any) { if (error.code !== 'ENOENT') throw error } return undefined } /** * Resolves the specified path as an {@link AbsolutePath} and checks it is a * _directory_, returning `undefined` if it is not. */ export function resolveDirectory(path: AbsolutePath, ...paths: string[]): AbsolutePath | undefined { const directory = resolveAbsolutePath(path, ...paths) try { const stat = statSync(directory) if (stat.isDirectory()) return directory } catch (error: any) { if (error.code !== 'ENOENT') throw error } return undefined }