aboutsummaryrefslogtreecommitdiff
path: root/packages/internal-helpers
diff options
context:
space:
mode:
Diffstat (limited to 'packages/internal-helpers')
-rw-r--r--packages/internal-helpers/CHANGELOG.md119
-rw-r--r--packages/internal-helpers/package.json51
-rw-r--r--packages/internal-helpers/readme.md3
-rw-r--r--packages/internal-helpers/src/fs.ts89
-rw-r--r--packages/internal-helpers/src/path.ts121
-rw-r--r--packages/internal-helpers/src/remote.ts131
-rw-r--r--packages/internal-helpers/tsconfig.json7
7 files changed, 521 insertions, 0 deletions
diff --git a/packages/internal-helpers/CHANGELOG.md b/packages/internal-helpers/CHANGELOG.md
new file mode 100644
index 000000000..18c3b9bdd
--- /dev/null
+++ b/packages/internal-helpers/CHANGELOG.md
@@ -0,0 +1,119 @@
+# @astrojs/internal-helpers
+
+## 0.6.1
+
+### Patch Changes
+
+- [#13355](https://github.com/withastro/astro/pull/13355) [`042d1de`](https://github.com/withastro/astro/commit/042d1de901fd9aa66157ce078b28bcd9786e1373) Thanks [@ematipico](https://github.com/ematipico)! - Adds documentation to the assets utilities for remote service images.
+
+## 0.6.0
+
+### Minor Changes
+
+- [#13254](https://github.com/withastro/astro/pull/13254) [`1e11f5e`](https://github.com/withastro/astro/commit/1e11f5e8b722b179e382f3c792cd961b2b51f61b) Thanks [@p0lyw0lf](https://github.com/p0lyw0lf)! - Adds remote URL filtering utilities
+
+ This adds logic to filter remote URLs so that it can be used by both `astro` and `@astrojs/markdown-remark`.
+
+## 0.5.1
+
+### Patch Changes
+
+- [#13130](https://github.com/withastro/astro/pull/13130) [`b71bd10`](https://github.com/withastro/astro/commit/b71bd10989c0070847cecb101afb8278d5ef7091) Thanks [@ascorbic](https://github.com/ascorbic)! - Fixes a bug that meant that internal as well as trailing duplicate slashes were collapsed
+
+## 0.5.0
+
+### Minor Changes
+
+- [#12994](https://github.com/withastro/astro/pull/12994) [`5361755`](https://github.com/withastro/astro/commit/536175528dbbe75aa978d615ba2517b64bad7879) Thanks [@ascorbic](https://github.com/ascorbic)! - Adds `collapseDuplicateTrailingSlashes` function
+
+## 0.4.2
+
+### Patch Changes
+
+- [#12559](https://github.com/withastro/astro/pull/12559) [`1dc8f5e`](https://github.com/withastro/astro/commit/1dc8f5eb7c515c89aadc85cfa0a300d4f65e8671) Thanks [@delucis](https://github.com/delucis)! - Fixes usage of `fileURLToPath()` to anticipate the changed signature of this method in Node 22.1.0
+
+## 0.4.1
+
+### Patch Changes
+
+- [#11323](https://github.com/withastro/astro/pull/11323) [`41064ce`](https://github.com/withastro/astro/commit/41064cee78c1cccd428f710a24c483aeb275fd95) Thanks [@ascorbic](https://github.com/ascorbic)! - Extracts fs helpers into shared internal-helpers module
+
+## 0.4.0
+
+### Minor Changes
+
+- [#10596](https://github.com/withastro/astro/pull/10596) [`20463a6c1e1271d8dc3cb0ab3419ee5c72abd218`](https://github.com/withastro/astro/commit/20463a6c1e1271d8dc3cb0ab3419ee5c72abd218) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Add `removeBase` function
+
+## 0.3.0
+
+### Minor Changes
+
+- [#10189](https://github.com/withastro/astro/pull/10189) [`1ea0a25b94125e4f6f2ac82b42f638e22d7bdffd`](https://github.com/withastro/astro/commit/1ea0a25b94125e4f6f2ac82b42f638e22d7bdffd) Thanks [@peng](https://github.com/peng)! - Adds the option to pass an object to `build.assetsPrefix`. This allows for the use of multiple CDN prefixes based on the target file type.
+
+ When passing an object to `build.assetsPrefix`, you must also specify a `fallback` domain to be used for all other file types not specified.
+
+ Specify a file extension as the key (e.g. 'js', 'png') and the URL serving your assets of that file type as the value:
+
+ ```js
+ // astro.config.mjs
+ import { defineConfig } from 'astro/config';
+
+ export default defineConfig({
+ build: {
+ assetsPrefix: {
+ js: 'https://js.cdn.example.com',
+ mjs: 'https://js.cdn.example.com', // if you have .mjs files, you must add a new entry like this
+ png: 'https://images.cdn.example.com',
+ fallback: 'https://generic.cdn.example.com',
+ },
+ },
+ });
+ ```
+
+## 0.2.1
+
+### Patch Changes
+
+- [#8737](https://github.com/withastro/astro/pull/8737) [`6f60da805`](https://github.com/withastro/astro/commit/6f60da805e0014bc50dd07bef972e91c73560c3c) Thanks [@ematipico](https://github.com/ematipico)! - Add provenance statement when publishing the library from CI
+
+## 0.2.0
+
+### Minor Changes
+
+- [#8188](https://github.com/withastro/astro/pull/8188) [`d0679a666`](https://github.com/withastro/astro/commit/d0679a666f37da0fca396d42b9b32bbb25d29312) Thanks [@ematipico](https://github.com/ematipico)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023.
+
+- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate
+
+### Patch Changes
+
+- [#8062](https://github.com/withastro/astro/pull/8062) [`2aa6d8ace`](https://github.com/withastro/astro/commit/2aa6d8ace398a41c2dec5473521d758816b08191) Thanks [@bluwy](https://github.com/bluwy)! - Trigger re-release to fix `collapseDuplicateSlashes` export
+
+## 0.2.0-rc.2
+
+### Minor Changes
+
+- [#8179](https://github.com/withastro/astro/pull/8179) [`6011d52d3`](https://github.com/withastro/astro/commit/6011d52d38e43c3e3d52bc3bc41a60e36061b7b7) Thanks [@matthewp](https://github.com/matthewp)! - Astro 3.0 Release Candidate
+
+## 0.2.0-beta.1
+
+### Patch Changes
+
+- [#8062](https://github.com/withastro/astro/pull/8062) [`2aa6d8ace`](https://github.com/withastro/astro/commit/2aa6d8ace398a41c2dec5473521d758816b08191) Thanks [@bluwy](https://github.com/bluwy)! - Trigger re-release to fix `collapseDuplicateSlashes` export
+
+## 0.2.0-beta.0
+
+### Minor Changes
+
+- [`1eae2e3f7`](https://github.com/withastro/astro/commit/1eae2e3f7d693c9dfe91c8ccfbe606d32bf2fb81) Thanks [@Princesseuh](https://github.com/Princesseuh)! - Remove support for Node 16. The lowest supported version by Astro and all integrations is now v18.14.1. As a reminder, Node 16 will be deprecated on the 11th September 2023.
+
+## 0.1.2
+
+### Patch Changes
+
+- [#7935](https://github.com/withastro/astro/pull/7935) [`6035bb35f`](https://github.com/withastro/astro/commit/6035bb35f222fc6a80b418f13998b21c59da85b6) Thanks [@natemoo-re](https://github.com/natemoo-re)! - Add `collapseDuplicateSlashes` helper
+
+## 0.1.1
+
+### Patch Changes
+
+- [#7440](https://github.com/withastro/astro/pull/7440) [`2b7539952`](https://github.com/withastro/astro/commit/2b75399520bebfc537cca8204e483f0df3373904) Thanks [@bluwy](https://github.com/bluwy)! - Add `slash` path utility
diff --git a/packages/internal-helpers/package.json b/packages/internal-helpers/package.json
new file mode 100644
index 000000000..5cb726164
--- /dev/null
+++ b/packages/internal-helpers/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@astrojs/internal-helpers",
+ "description": "Internal helpers used by core Astro packages.",
+ "version": "0.6.1",
+ "type": "module",
+ "author": "withastro",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/withastro/astro.git",
+ "directory": "packages/internal-helpers"
+ },
+ "bugs": "https://github.com/withastro/astro/issues",
+ "exports": {
+ "./path": "./dist/path.js",
+ "./remote": "./dist/remote.js",
+ "./fs": "./dist/fs.js"
+ },
+ "typesVersions": {
+ "*": {
+ "path": [
+ "./dist/path.d.ts"
+ ],
+ "remote": [
+ "./dist/remote.d.ts"
+ ],
+ "fs": [
+ "./dist/fs.d.ts"
+ ]
+ }
+ },
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "prepublish": "pnpm build",
+ "build": "astro-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json",
+ "build:ci": "astro-scripts build \"src/**/*.ts\"",
+ "dev": "astro-scripts dev \"src/**/*.ts\""
+ },
+ "devDependencies": {
+ "astro-scripts": "workspace:*"
+ },
+ "keywords": [
+ "astro",
+ "astro-component"
+ ],
+ "publishConfig": {
+ "provenance": true
+ }
+}
diff --git a/packages/internal-helpers/readme.md b/packages/internal-helpers/readme.md
new file mode 100644
index 000000000..283913dc5
--- /dev/null
+++ b/packages/internal-helpers/readme.md
@@ -0,0 +1,3 @@
+# @astrojs/internal-helpers
+
+These are internal helpers used by core Astro packages. This package does not follow semver and should not be used externally.
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);
+}
diff --git a/packages/internal-helpers/src/remote.ts b/packages/internal-helpers/src/remote.ts
new file mode 100644
index 000000000..6192aaf1b
--- /dev/null
+++ b/packages/internal-helpers/src/remote.ts
@@ -0,0 +1,131 @@
+export type RemotePattern = {
+ hostname?: string;
+ pathname?: string;
+ protocol?: string;
+ port?: string;
+};
+
+/**
+ * Evaluates whether a given URL matches the specified remote pattern based on protocol, hostname, port, and pathname.
+ *
+ * @param {URL} url - The URL object to be matched against the remote pattern.
+ * @param {RemotePattern} remotePattern - The remote pattern object containing the protocol, hostname, port, and pathname to match.
+ * @return {boolean} Returns `true` if the URL matches the given remote pattern; otherwise, `false`.
+ */
+export function matchPattern(url: URL, remotePattern: RemotePattern): boolean {
+ return (
+ matchProtocol(url, remotePattern.protocol) &&
+ matchHostname(url, remotePattern.hostname, true) &&
+ matchPort(url, remotePattern.port) &&
+ matchPathname(url, remotePattern.pathname, true)
+ );
+}
+
+/**
+ * Checks if the given URL's port matches the specified port. If no port is provided, it returns `true`.
+ *
+ * @param {URL} url - The URL object whose port will be checked.
+ * @param {string} [port=] - The port to match against the URL's port. Optional.
+ * @return {boolean} Returns `true` if the URL's port matches the specified port or if no port is provided; otherwise, `false`.
+ */
+export function matchPort(url: URL, port?: string): boolean {
+ return !port || port === url.port;
+}
+
+/**
+ * Compares the protocol of the provided URL with a specified protocol.
+ *
+ * @param {URL} url - The URL object whose protocol needs to be checked.
+ * @param {string} [protocol] - The protocol to compare against, without the trailing colon. If not provided, the method will always return `true`.
+ * @return {boolean} Returns `true` if the protocol matches or if no protocol is specified; otherwise, `false`.
+ */
+export function matchProtocol(url: URL, protocol?: string): boolean {
+ return !protocol || protocol === url.protocol.slice(0, -1);
+}
+
+/**
+ * Matches a given URL's hostname against a specified hostname, with optional support for wildcard patterns.
+ *
+ * @param {URL} url - The URL object whose hostname is to be matched.
+ * @param {string} [hostname] - The hostname to match against. Supports wildcard patterns if `allowWildcard` is `true`.
+ * @param {boolean} [allowWildcard=false] - Indicates whether wildcard patterns in the `hostname` parameter are allowed.
+ * @return {boolean} - Returns `true` if the URL's hostname matches the given hostname criteria; otherwise, `false`.
+ */
+export function matchHostname(url: URL, hostname?: string, allowWildcard = false): boolean {
+ if (!hostname) {
+ return true;
+ } else if (!allowWildcard || !hostname.startsWith('*')) {
+ return hostname === url.hostname;
+ } else if (hostname.startsWith('**.')) {
+ const slicedHostname = hostname.slice(2); // ** length
+ return slicedHostname !== url.hostname && url.hostname.endsWith(slicedHostname);
+ } else if (hostname.startsWith('*.')) {
+ const slicedHostname = hostname.slice(1); // * length
+ const additionalSubdomains = url.hostname
+ .replace(slicedHostname, '')
+ .split('.')
+ .filter(Boolean);
+ return additionalSubdomains.length === 1;
+ }
+
+ return false;
+}
+
+/**
+ * Matches a given URL's pathname against a specified pattern, with optional support for wildcards.
+ *
+ * @param {URL} url - The URL object containing the pathname to be matched.
+ * @param {string} [pathname] - The pathname pattern to match the URL against.
+ * @param {boolean} [allowWildcard=false] - Determines whether wildcard matching is allowed.
+ * @return {boolean} - Returns `true` if the URL's pathname matches the specified pattern; otherwise, `false`.
+ */
+export function matchPathname(url: URL, pathname?: string, allowWildcard = false): boolean {
+ if (!pathname) {
+ return true;
+ } else if (!allowWildcard || !pathname.endsWith('*')) {
+ return pathname === url.pathname;
+ } else if (pathname.endsWith('/**')) {
+ const slicedPathname = pathname.slice(0, -2); // ** length
+ return slicedPathname !== url.pathname && url.pathname.startsWith(slicedPathname);
+ } else if (pathname.endsWith('/*')) {
+ const slicedPathname = pathname.slice(0, -1); // * length
+ const additionalPathChunks = url.pathname
+ .replace(slicedPathname, '')
+ .split('/')
+ .filter(Boolean);
+ return additionalPathChunks.length === 1;
+ }
+
+ return false;
+}
+
+/**
+ * Determines whether a given remote resource, identified by its source URL,
+ * is allowed based on specified domains and remote patterns.
+ *
+ * @param {string} src - The source URL of the remote resource to be validated.
+ * @param {Object} options - The configuration options for domain and pattern matching.
+ * @param {string[]} options.domains - A list of allowed domain names.
+ * @param {RemotePattern[]} options.remotePatterns - A list of allowed remote patterns for matching.
+ * @return {boolean} Returns `true` if the source URL matches any of the specified domains or remote patterns; otherwise, `false`.
+ */
+export function isRemoteAllowed(
+ src: string,
+ {
+ domains,
+ remotePatterns,
+ }: {
+ domains: string[];
+ remotePatterns: RemotePattern[];
+ },
+): boolean {
+ if (!URL.canParse(src)) {
+ return false;
+ }
+
+ const url = new URL(src);
+ return (
+ domains.some((domain) => matchHostname(url, domain)) ||
+ remotePatterns.some((remotePattern) => matchPattern(url, remotePattern))
+ );
+}
diff --git a/packages/internal-helpers/tsconfig.json b/packages/internal-helpers/tsconfig.json
new file mode 100644
index 000000000..18443cddf
--- /dev/null
+++ b/packages/internal-helpers/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "include": ["src"],
+ "compilerOptions": {
+ "outDir": "./dist"
+ }
+}