Skip to content
Merged
2 changes: 0 additions & 2 deletions packages/url-utils/index.js

This file was deleted.

9 changes: 4 additions & 5 deletions packages/url-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,19 @@
},
"author": "Ghost Foundation",
"license": "MIT",
"main": "index.js",
"types": "lib/UrlUtils.d.ts",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"scripts": {
"dev": "echo \"Implement me!\"",
"pretest": "yarn build",
"test": "NODE_ENV=testing c8 --src lib --all --reporter text --reporter cobertura --reporter html mocha './test/**/*.test.js'",
"build": "tsc -p tsconfig.json",
"lint": "eslint src test index.js --ext .js,.ts --cache",
"lint": "eslint src test --ext .js,.ts --cache",
"prepare": "NODE_ENV=production yarn build",
"posttest": "yarn lint"
},
"files": [
"lib/",
"index.js"
"lib/"
],
"publishConfig": {
"access": "public"
Expand Down
464 changes: 303 additions & 161 deletions packages/url-utils/src/UrlUtils.ts

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/url-utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import UrlUtils from './UrlUtils';

export = UrlUtils;
29 changes: 20 additions & 9 deletions packages/url-utils/src/utils/absolute-to-relative.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
// @ts-nocheck
// require the whatwg compatible URL library (same behaviour in node and browser)
const {URL} = require('url');
const stripSubdirectoryFromPath = require('./strip-subdirectory-from-path');
import {URL} from 'url';
import stripSubdirectoryFromPath from './strip-subdirectory-from-path';

export interface AbsoluteToRelativeOptions {
ignoreProtocol: boolean;
withoutSubdirectory: boolean;
assetsOnly: boolean;
staticImageUrlPrefix: string;
}

export type AbsoluteToRelativeOptionsInput = Partial<AbsoluteToRelativeOptions>;

/**
* Convert an absolute URL to a root-relative path if it matches the supplied root domain.
Expand All @@ -13,8 +20,8 @@ const stripSubdirectoryFromPath = require('./strip-subdirectory-from-path');
* @param {boolean} [options.withoutSubdirectory=false] Strip the root subdirectory from the returned path
* @returns {string} The passed-in url or a relative path
*/
const absoluteToRelative = function absoluteToRelative(url, rootUrl, _options = {}) {
const defaultOptions = {
const absoluteToRelative = function absoluteToRelative(url: string, rootUrl?: string, _options: AbsoluteToRelativeOptionsInput = {}): string {
const defaultOptions: AbsoluteToRelativeOptions = {
ignoreProtocol: true,
withoutSubdirectory: false,
assetsOnly: false,
Expand All @@ -29,8 +36,8 @@ const absoluteToRelative = function absoluteToRelative(url, rootUrl, _options =
}
}

let parsedUrl;
let parsedRoot;
let parsedUrl: URL;
let parsedRoot: URL | undefined;

try {
parsedUrl = new URL(url, 'http://relative');
Expand All @@ -44,6 +51,10 @@ const absoluteToRelative = function absoluteToRelative(url, rootUrl, _options =
return url;
}

if (!parsedRoot) {
return url;
}

const matchesHost = parsedUrl.host === parsedRoot.host;
const matchesProtocol = parsedUrl.protocol === parsedRoot.protocol;
const matchesPath = parsedUrl.pathname.indexOf(parsedRoot.pathname) === 0;
Expand All @@ -61,4 +72,4 @@ const absoluteToRelative = function absoluteToRelative(url, rootUrl, _options =
return url;
};

module.exports = absoluteToRelative;
export default absoluteToRelative;
46 changes: 33 additions & 13 deletions packages/url-utils/src/utils/absolute-to-transform-ready.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
// @ts-nocheck
const {URL} = require('url');
const absoluteToRelative = require('./absolute-to-relative');
import type {TransformReadyReplacementOptions} from './types';
import absoluteToRelative, {type AbsoluteToRelativeOptionsInput} from './absolute-to-relative';
import {URL} from 'url';

function isRelative(url) {
let parsedInput;
export interface AbsoluteToTransformReadyOptions extends TransformReadyReplacementOptions {
withoutSubdirectory: boolean;
ignoreProtocol: boolean;
assetsOnly: boolean;
staticImageUrlPrefix: string;
staticFilesUrlPrefix?: string;
staticMediaUrlPrefix?: string;
imageBaseUrl?: string | null;
filesBaseUrl?: string | null;
mediaBaseUrl?: string | null;
}

export type AbsoluteToTransformReadyOptionsInput = Partial<AbsoluteToTransformReadyOptions>;

function isRelative(url: string): boolean {
let parsedInput: URL;
try {
parsedInput = new URL(url, 'http://relative');
} catch (e) {
Expand All @@ -14,16 +28,22 @@ function isRelative(url) {
return parsedInput.origin === 'http://relative';
}

const absoluteToTransformReady = function (url, root, _options = {}) {
const defaultOptions = {
const absoluteToTransformReady = function (
url: string,
root: string,
_options: AbsoluteToTransformReadyOptionsInput = {}
): string {
const defaultOptions: AbsoluteToTransformReadyOptions = {
replacementStr: '__GHOST_URL__',
withoutSubdirectory: true,
staticImageUrlPrefix: 'content/images',
staticFilesUrlPrefix: 'content/files',
staticMediaUrlPrefix: 'content/media',
imageBaseUrl: null,
filesBaseUrl: null,
mediaBaseUrl: null
mediaBaseUrl: null,
ignoreProtocol: true,
assetsOnly: false
};
const options = Object.assign({}, defaultOptions, _options);

Expand All @@ -33,30 +53,30 @@ const absoluteToTransformReady = function (url, root, _options = {}) {

// convert to relative with stripped subdir
// always returns root-relative starting with forward slash
const rootRelativeUrl = absoluteToRelative(url, root, options);
const rootRelativeUrl = absoluteToRelative(url, root, options as AbsoluteToRelativeOptionsInput);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to type cast here? Can we instead make sure that options is of type AbsoluteToTransformReadyOptions and then it overlaps?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I've updated AbsoluteToTransformReadyOptions to extend AbsoluteToRelativeOptions, which makes the types properly overlap. Removed type casts.


if (isRelative(rootRelativeUrl)) {
return `${options.replacementStr}${rootRelativeUrl}`;
}

if (options.mediaBaseUrl) {
const mediaRelativeUrl = absoluteToRelative(url, options.mediaBaseUrl, options);
const mediaRelativeUrl = absoluteToRelative(url, options.mediaBaseUrl, options as AbsoluteToRelativeOptionsInput);

if (isRelative(mediaRelativeUrl)) {
return `${options.replacementStr}${mediaRelativeUrl}`;
}
}

if (options.filesBaseUrl) {
const filesRelativeUrl = absoluteToRelative(url, options.filesBaseUrl, options);
const filesRelativeUrl = absoluteToRelative(url, options.filesBaseUrl, options as AbsoluteToRelativeOptionsInput);

if (isRelative(filesRelativeUrl)) {
return `${options.replacementStr}${filesRelativeUrl}`;
}
}

if (options.imageBaseUrl) {
const imageRelativeUrl = absoluteToRelative(url, options.imageBaseUrl, options);
const imageRelativeUrl = absoluteToRelative(url, options.imageBaseUrl, options as AbsoluteToRelativeOptionsInput);

if (isRelative(imageRelativeUrl)) {
return `${options.replacementStr}${imageRelativeUrl}`;
Expand All @@ -66,4 +86,4 @@ const absoluteToTransformReady = function (url, root, _options = {}) {
return url;
};

module.exports = absoluteToTransformReady;
export default absoluteToTransformReady;
19 changes: 12 additions & 7 deletions packages/url-utils/src/utils/build-early-exit-match.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
// @ts-nocheck
function escapeRegExp(string) {
import type {BaseUrlOptionsInput} from './types';

function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

type BuildEarlyExitMatchOptions = BaseUrlOptionsInput & {
ignoreProtocol?: boolean;
};

/**
* Build a regex pattern that matches any of the configured base URLs (site URL + CDN URLs).
* This is used for early exit optimizations - if content doesn't contain any of these URLs,
Expand All @@ -16,14 +21,14 @@ function escapeRegExp(string) {
* @param {boolean} [options.ignoreProtocol=true] - Whether to strip protocol from URLs
* @returns {string|null} Regex pattern matching any configured base URL, or null if none configured
*/
function buildEarlyExitMatch(siteUrl, options = {}) {
function buildEarlyExitMatch(siteUrl: string, options: BuildEarlyExitMatchOptions = {}): string | null {
const candidates = [siteUrl, options.imageBaseUrl, options.filesBaseUrl, options.mediaBaseUrl]
.filter(Boolean)
.map((value) => {
.filter((value): value is string => Boolean(value))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

value is string doesn't match the actual implementation 🤔

The Boolean(value) check is making sure it's a non empty string - I think we should change this line to the following which is slightly more correct and at least enforces the string type

.filter((value: string | null): value is string => typeof value === 'string' && value.length > 0)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

.map((value: string) => {
let normalized = options.ignoreProtocol ? value.replace(/http:|https:/, '') : value;
return normalized.replace(/\/$/, '');
})
.filter(Boolean)
.filter((value): value is string => Boolean(value))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in this case we can just do (value: string): boolean => Boolean(value)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

.map(escapeRegExp);

if (!candidates.length) {
Expand All @@ -37,7 +42,7 @@ function buildEarlyExitMatch(siteUrl, options = {}) {
return `(?:${candidates.join('|')})`;
}

module.exports = {
export default {
buildEarlyExitMatch,
escapeRegExp
};
5 changes: 2 additions & 3 deletions packages/url-utils/src/utils/deduplicate-double-slashes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// @ts-nocheck
function deduplicateDoubleSlashes(url) {
function deduplicateDoubleSlashes(url: string): string {
// Preserve protocol slashes (e.g., http://, https://) and only deduplicate
// slashes in the path portion. The pattern (^|[^:])\/\/+ matches double slashes
// that are either at the start of the string or not preceded by a colon.
return url.replace(/(^|[^:])\/\/+/g, '$1/');
}

module.exports = deduplicateDoubleSlashes;
export default deduplicateDoubleSlashes;
7 changes: 3 additions & 4 deletions packages/url-utils/src/utils/deduplicate-subdirectory.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// @ts-nocheck
const {URL} = require('url');
import {URL} from 'url';

/**
* Remove duplicated directories from the start of a path or url's path
Expand All @@ -8,7 +7,7 @@ const {URL} = require('url');
* @param {string} rootUrl Root URL with an optional subdirectory
* @returns {string} URL or pathname with any duplicated subdirectory removed
*/
const deduplicateSubdirectory = function deduplicateSubdirectory(url, rootUrl) {
const deduplicateSubdirectory = function deduplicateSubdirectory(url: string, rootUrl: string): string {
// force root url to always have a trailing-slash for consistent behaviour
if (!rootUrl.endsWith('/')) {
rootUrl = `${rootUrl}/`;
Expand All @@ -29,4 +28,4 @@ const deduplicateSubdirectory = function deduplicateSubdirectory(url, rootUrl) {
return url.replace(subdirRegex, `$1${subdir}/`);
};

module.exports = deduplicateSubdirectory;
export default deduplicateSubdirectory;
21 changes: 13 additions & 8 deletions packages/url-utils/src/utils/html-absolute-to-relative.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
// @ts-nocheck
const htmlTransform = require('./html-transform');
const absoluteToRelative = require('./absolute-to-relative');
import type {HtmlTransformOptionsInput} from './types';
import type {AbsoluteToRelativeOptionsInput} from './absolute-to-relative';
import htmlTransform from './html-transform';
import absoluteToRelative from './absolute-to-relative';

function htmlAbsoluteToRelative(html = '', siteUrl, _options) {
const defaultOptions = {assetsOnly: false, ignoreProtocol: true};
const options = Object.assign({}, defaultOptions, _options || {});
function htmlAbsoluteToRelative(
html: string = '',
siteUrl: string,
_options: AbsoluteToRelativeOptionsInput = {}
): string {
const defaultOptions: AbsoluteToRelativeOptionsInput = {assetsOnly: false, ignoreProtocol: true};
const options: HtmlTransformOptionsInput = Object.assign({}, defaultOptions, _options || {});

// exit early and avoid parsing if the content does not contain the siteUrl
options.earlyExitMatchStr = options.ignoreProtocol ? siteUrl.replace(/http:|https:/, '') : siteUrl;
options.earlyExitMatchStr = options.earlyExitMatchStr.replace(/\/$/, '');

// need to ignore itemPath because absoluteToRelative doesn't take that option
const transformFunction = function (_url, _siteUrl, _itemPath, __options) {
const transformFunction = function (_url: string, _siteUrl: string, _itemPath: string | null, __options: AbsoluteToRelativeOptionsInput): string {
return absoluteToRelative(_url, _siteUrl, __options);
};

return htmlTransform(html, siteUrl, transformFunction, '', options);
}

module.exports = htmlAbsoluteToRelative;
export default htmlAbsoluteToRelative;
28 changes: 18 additions & 10 deletions packages/url-utils/src/utils/html-absolute-to-transform-ready.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
// @ts-nocheck
const htmlTransform = require('./html-transform');
const absoluteToTransformReady = require('./absolute-to-transform-ready');
const {buildEarlyExitMatch} = require('./build-early-exit-match');
import type {HtmlTransformOptionsInput, AbsoluteToTransformReadyOptionsInput} from './types';
import htmlTransform from './html-transform';
import absoluteToTransformReady from './absolute-to-transform-ready';
import buildEarlyExitMatchModule from './build-early-exit-match';
const {buildEarlyExitMatch} = buildEarlyExitMatchModule;

const htmlAbsoluteToTransformReady = function (html = '', siteUrl, _options) {
const defaultOptions = {assetsOnly: false, ignoreProtocol: true};
const options = Object.assign({}, defaultOptions, _options || {});
const htmlAbsoluteToTransformReady = function (
html: string = '',
siteUrl: string,
_options: AbsoluteToTransformReadyOptionsInput = {}
): string {
const defaultOptions: AbsoluteToTransformReadyOptionsInput = {assetsOnly: false, ignoreProtocol: true};
const options: HtmlTransformOptionsInput = Object.assign({}, defaultOptions, _options || {});

// exit early and avoid parsing if the content does not contain the siteUrl or configured asset bases
options.earlyExitMatchStr = buildEarlyExitMatch(siteUrl, options);
const earlyExitMatch = buildEarlyExitMatch(siteUrl, options);
if (earlyExitMatch) {
options.earlyExitMatchStr = earlyExitMatch;
}

// need to ignore itemPath because absoluteToRelative doesn't take that option
const transformFunction = function (_url, _siteUrl, _itemPath, __options) {
const transformFunction = function (_url: string, _siteUrl: string, _itemPath: string | null, __options: AbsoluteToTransformReadyOptionsInput): string {
return absoluteToTransformReady(_url, _siteUrl, __options);
};

return htmlTransform(html, siteUrl, transformFunction, '', options);
};

module.exports = htmlAbsoluteToTransformReady;
export default htmlAbsoluteToTransformReady;
19 changes: 12 additions & 7 deletions packages/url-utils/src/utils/html-relative-to-absolute.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
// @ts-nocheck
const htmlTransform = require('./html-transform');
const relativeToAbsolute = require('./relative-to-absolute');
import type {HtmlTransformOptionsInput, SecureOptionsInput} from './types';
import htmlTransform from './html-transform';
import relativeToAbsolute from './relative-to-absolute';

function htmlRelativeToAbsolute(html = '', siteUrl, itemPath, _options) {
const defaultOptions = {assetsOnly: false, secure: false};
const options = Object.assign({}, defaultOptions, _options || {});
function htmlRelativeToAbsolute(
html: string = '',
siteUrl: string,
itemPath: string | null,
_options: SecureOptionsInput = {}
): string {
const defaultOptions: SecureOptionsInput = {assetsOnly: false, secure: false};
const options: HtmlTransformOptionsInput = Object.assign({}, defaultOptions, _options || {});

// exit early and avoid parsing if the content does not contain an attribute we might transform
options.earlyExitMatchStr = 'href=|src=|srcset=';
Expand All @@ -15,4 +20,4 @@ function htmlRelativeToAbsolute(html = '', siteUrl, itemPath, _options) {
return htmlTransform(html, siteUrl, relativeToAbsolute, itemPath, options);
}

module.exports = htmlRelativeToAbsolute;
export default htmlRelativeToAbsolute;
Loading
Loading