aboutsummaryrefslogtreecommitdiff
path: root/packages/internal-helpers/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/internal-helpers/src')
-rw-r--r--packages/internal-helpers/src/fs.ts89
-rw-r--r--packages/internal-helpers/src/path.ts121
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);
+}