diff options
author | 2025-06-05 14:25:23 +0000 | |
---|---|---|
committer | 2025-06-05 14:25:23 +0000 | |
commit | e586d7d704d475afe3373a1de6ae20d504f79d6d (patch) | |
tree | 7e3fa24807cebd48a86bd40f866d792181191ee9 /packages/internal-helpers | |
download | astro-latest.tar.gz astro-latest.tar.zst astro-latest.zip |
Sync from a8e1c0a7402940e0fc5beef669522b315052df1blatest
Diffstat (limited to 'packages/internal-helpers')
-rw-r--r-- | packages/internal-helpers/CHANGELOG.md | 119 | ||||
-rw-r--r-- | packages/internal-helpers/package.json | 51 | ||||
-rw-r--r-- | packages/internal-helpers/readme.md | 3 | ||||
-rw-r--r-- | packages/internal-helpers/src/fs.ts | 89 | ||||
-rw-r--r-- | packages/internal-helpers/src/path.ts | 121 | ||||
-rw-r--r-- | packages/internal-helpers/src/remote.ts | 131 | ||||
-rw-r--r-- | packages/internal-helpers/tsconfig.json | 7 |
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" + } +} |