diff options
Diffstat (limited to 'packages/internal-helpers/src')
-rw-r--r-- | packages/internal-helpers/src/fs.ts | 89 | ||||
-rw-r--r-- | packages/internal-helpers/src/path.ts | 121 |
2 files changed, 210 insertions, 0 deletions
diff --git a/packages/internal-helpers/src/fs.ts b/packages/internal-helpers/src/fs.ts new file mode 100644 index 000000000..5630040bb --- /dev/null +++ b/packages/internal-helpers/src/fs.ts @@ -0,0 +1,89 @@ +import type { PathLike } from 'node:fs'; +import { existsSync } from 'node:fs'; +import * as fs from 'node:fs/promises'; +import nodePath from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export async function writeJson<T>(path: PathLike, data: T) { + await fs.writeFile(path, JSON.stringify(data, null, '\t'), { encoding: 'utf-8' }); +} + +export async function removeDir(dir: PathLike) { + await fs.rm(dir, { recursive: true, force: true, maxRetries: 3 }); +} + +export async function emptyDir(dir: PathLike): Promise<void> { + await removeDir(dir); + await fs.mkdir(dir, { recursive: true }); +} + +export async function getFilesFromFolder(dir: URL) { + const data = await fs.readdir(dir, { withFileTypes: true }); + let files: URL[] = []; + for (const item of data) { + if (item.isDirectory()) { + const moreFiles = await getFilesFromFolder(new URL(`./${item.name}/`, dir)); + files = files.concat(moreFiles); + } else { + files.push(new URL(`./${item.name}`, dir)); + } + } + return files; +} + +/** + * Copies files into a folder keeping the folder structure intact. + * The resulting file tree will start at the common ancestor. + * + * @param {URL[]} files A list of files to copy (absolute path). + * @param {URL} outDir Destination folder where to copy the files to (absolute path). + * @param {URL[]} [exclude] A list of files to exclude (absolute path). + * @returns {Promise<string>} The common ancestor of the copied files. + */ +export async function copyFilesToFolder( + files: URL[], + outDir: URL, + exclude: URL[] = [], +): Promise<string> { + const excludeList = exclude.map((url) => fileURLToPath(url)); + const fileList = files.map((url) => fileURLToPath(url)).filter((f) => !excludeList.includes(f)); + + if (files.length === 0) throw new Error('No files found to copy'); + + let commonAncestor = nodePath.dirname(fileList[0]); + for (const file of fileList.slice(1)) { + while (!file.startsWith(commonAncestor)) { + commonAncestor = nodePath.dirname(commonAncestor); + } + } + + for (const origin of fileList) { + const dest = new URL(nodePath.relative(commonAncestor, origin), outDir); + + const realpath = await fs.realpath(origin); + const isSymlink = realpath !== origin; + const isDir = (await fs.stat(origin)).isDirectory(); + + // Create directories recursively + if (isDir && !isSymlink) { + await fs.mkdir(new URL('..', dest), { recursive: true }); + } else { + await fs.mkdir(new URL('.', dest), { recursive: true }); + } + + if (isSymlink) { + const realdest = fileURLToPath(new URL(nodePath.relative(commonAncestor, realpath), outDir)); + const target = nodePath.relative(fileURLToPath(new URL('.', dest)), realdest); + // NOTE: when building function per route, dependencies are linked at the first run, then there's no need anymore to do that once more. + // So we check if the destination already exists. If it does, move on. + // Symbolic links here are usually dependencies and not user code. Symbolic links exist because of the pnpm strategy. + if (!existsSync(dest)) { + await fs.symlink(target, dest, isDir ? 'dir' : 'file'); + } + } else if (!isDir) { + await fs.copyFile(origin, dest); + } + } + + return commonAncestor; +} diff --git a/packages/internal-helpers/src/path.ts b/packages/internal-helpers/src/path.ts new file mode 100644 index 000000000..c7dda9d89 --- /dev/null +++ b/packages/internal-helpers/src/path.ts @@ -0,0 +1,121 @@ +/** + * A set of common path utilities commonly used through the Astro core and integration + * projects. These do things like ensure a forward slash prepends paths. + */ + +export function appendExtension(path: string, extension: string) { + return path + '.' + extension; +} + +export function appendForwardSlash(path: string) { + return path.endsWith('/') ? path : path + '/'; +} + +export function prependForwardSlash(path: string) { + return path[0] === '/' ? path : '/' + path; +} + +export function collapseDuplicateSlashes(path: string) { + return path.replace(/(?<!:)\/{2,}/g, '/'); +} + +export const MANY_TRAILING_SLASHES = /\/{2,}$/g; + +export function collapseDuplicateTrailingSlashes(path: string, trailingSlash: boolean) { + if (!path) { + return path; + } + return path.replace(MANY_TRAILING_SLASHES, trailingSlash ? '/' : '') || '/'; +} + +export function removeTrailingForwardSlash(path: string) { + return path.endsWith('/') ? path.slice(0, path.length - 1) : path; +} + +export function removeLeadingForwardSlash(path: string) { + return path.startsWith('/') ? path.substring(1) : path; +} + +export function removeLeadingForwardSlashWindows(path: string) { + return path.startsWith('/') && path[2] === ':' ? path.substring(1) : path; +} + +export function trimSlashes(path: string) { + return path.replace(/^\/|\/$/g, ''); +} + +export function startsWithForwardSlash(path: string) { + return path[0] === '/'; +} + +export function startsWithDotDotSlash(path: string) { + const c1 = path[0]; + const c2 = path[1]; + const c3 = path[2]; + return c1 === '.' && c2 === '.' && c3 === '/'; +} + +export function startsWithDotSlash(path: string) { + const c1 = path[0]; + const c2 = path[1]; + return c1 === '.' && c2 === '/'; +} + +export function isRelativePath(path: string) { + return startsWithDotDotSlash(path) || startsWithDotSlash(path); +} + +function isString(path: unknown): path is string { + return typeof path === 'string' || path instanceof String; +} + +export function joinPaths(...paths: (string | undefined)[]) { + return paths + .filter(isString) + .map((path, i) => { + if (i === 0) { + return removeTrailingForwardSlash(path); + } else if (i === paths.length - 1) { + return removeLeadingForwardSlash(path); + } else { + return trimSlashes(path); + } + }) + .join('/'); +} + +export function removeFileExtension(path: string) { + let idx = path.lastIndexOf('.'); + return idx === -1 ? path : path.slice(0, idx); +} + +export function removeQueryString(path: string) { + const index = path.lastIndexOf('?'); + return index > 0 ? path.substring(0, index) : path; +} + +export function isRemotePath(src: string) { + return /^(?:http|ftp|https|ws):?\/\//.test(src) || src.startsWith('data:'); +} + +export function slash(path: string) { + return path.replace(/\\/g, '/'); +} + +export function fileExtension(path: string) { + const ext = path.split('.').pop(); + return ext !== path ? `.${ext}` : ''; +} + +export function removeBase(path: string, base: string) { + if (path.startsWith(base)) { + return path.slice(removeTrailingForwardSlash(base).length); + } + return path; +} + +const WITH_FILE_EXT = /\/[^/]+\.\w+$/; + +export function hasFileExtension(path: string) { + return WITH_FILE_EXT.test(path); +} |