Skip to content

Instantly share code, notes, and snippets.

@brandonbryant12
Last active March 5, 2025 17:48
Show Gist options
  • Save brandonbryant12/f232e40319a88a30b51c3cb8c98fd14d to your computer and use it in GitHub Desktop.
Save brandonbryant12/f232e40319a88a30b51c3cb8c98fd14d to your computer and use it in GitHub Desktop.
const isRelativePath = (url) => !/^([a-z]+:\/\/|\/|#)/i.test(url);
// Main function to substitute relative paths with absolute paths according to Backstage rules
function substituteRelativePaths(entity, baseUrl) {
const result = JSON.parse(JSON.stringify(entity)); // Deep copy
// Substitute relative URLs in annotations (Backstage specific)
if (result.metadata?.annotations) {
for (const [key, value] of Object.entries(result.metadata.annotations)) {
if (typeof value === 'string' && isRelativePath(value)) {
result.metadata.annotations[key] = resolveUrl(baseUrl, value);
}
}
}
// Substitute relative URLs in links (Backstage specific)
if (Array.isArray(result.metadata?.links)) {
result.metadata.links = result.metadata.links.map(link => {
if (link.url && isRelativePath(link.url)) {
return { ...link, url: resolveUrl(baseUrl, link.url) };
}
return link;
});
}
// Substitute relative URLs in descriptor placeholders (Backstage rules)
const substitutePlaceholders = (obj) => {
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'string') {
const placeholderMatch = value.match(/\$(?:text|json|yaml)\((.+?)\)/);
if (placeholderMatch && isRelativePath(placeholderMatch[1])) {
const resolvedPath = resolveUrl(baseUrl, placeholderMatch[1]);
obj[key] = value.replace(placeholderMatch[1], resolvedPath);
}
} else if (typeof value === 'object' && value !== null) {
substitutePlaceholders(value);
}
}
};
substitutePlaceholders(result);
return result;
}
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: aggregator-test
description: |
This entity demonstrates various annotations and links with relative/absolute URLs
annotations:
# Relative file reference (should be replaced if isRelativePath is true):
'example.com/relative-annotation': './docs/relative-annotation.md'
# Absolute reference (should NOT be replaced):
'example.com/absolute-annotation': 'https://example.com/docs/absolute-annotation.md'
# Slash-based path (treated as absolute for the purpose of your regex, not replaced):
'example.com/slash-annotation': '/docs/slash-based-path.md'
# Anchor-based path (not replaced):
'example.com/anchor-annotation': '#section-anchor'
links:
# A relative link (should be replaced):
- url: './relative-link.md'
title: Relative Link Example
# An absolute link (should NOT be replaced):
- url: 'https://example.com/absolute-link'
title: Absolute Link Example
# Slash-based absolute link (should NOT be replaced):
- url: '/absolute/path'
title: Slash Link Example
# Anchor link (should NOT be replaced):
- url: '#my-anchor'
title: Anchor Link Example
spec:
type: service
owner: team-a
lifecycle: experimental
# Examples of placeholders
data:
# A placeholder referencing a relative file (will be replaced):
textPlaceholder: "$(text(./files/relative-file.txt))"
# A placeholder referencing an absolute file (should NOT be changed):
yamlPlaceholder: "$(yaml(https://example.com/data.yaml))"
# JSON placeholder with relative path (should be replaced):
jsonPlaceholder: "Load me: $(json(./files/some-json-file.json))"
# Nested object with a placeholder:
nested:
deeperPlaceholder: "$(text(./nested/deep-file.yaml))"
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: aggregator-other
annotations:
# Mixed content: relative placeholder embedded in a string
'example.com/mixed-placeholder': 'Path before $(text(./readme.md)) path after'
# Already-absolute in a placeholder (won't be replaced):
'example.com/abs-placeholder': 'Check $(yaml(https://example.com/another.yaml)) here'
links:
# No URL field (edge case, should simply skip)
- title: "No URL link"
spec:
type: openapi
definition: |
# Some inline spec
openapi: "3.0.0"
info:
title: "Aggregator-Other"
version: "1.0"
---
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: aggregator-test
description: >
This entity demonstrates various annotations and links
with relative/absolute URLs, plus descriptor placeholders.
annotations:
# Relative annotation (should be replaced):
'example.com/relative-annotation': './docs/relative-annotation.md'
# Absolute annotation (should NOT be replaced):
'example.com/absolute-annotation': 'https://example.com/docs/absolute-annotation.md'
# Slash-based annotation (treated as absolute, not replaced):
'example.com/slash-annotation': '/docs/slash-based-path.md'
# Anchor-based annotation (not replaced):
'example.com/anchor-annotation': '#section-anchor'
links:
# A relative link (should be replaced):
- url: './relative-link.md'
title: Relative Link Example
# An absolute link (should NOT be replaced):
- url: 'https://example.com/absolute-link'
title: Absolute Link Example
# Slash-based absolute link (should NOT be replaced):
- url: '/absolute/path'
title: Slash Link Example
# Anchor link (should NOT be replaced):
- url: '#my-anchor'
title: Anchor Link Example
spec:
type: service
owner: team-a
lifecycle: experimental
data:
# Descriptor placeholders:
textPlaceholder: "$text(./files/relative-file.txt)" # Relative, should be substituted
yamlPlaceholder: "$yaml(https://example.com/data.yaml)" # Already absolute, left unchanged
jsonPlaceholder: "Load me: $json(./files/some-json-file.json)" # Relative, should be substituted
nested:
deeperPlaceholder: "$text(./nested/deep-file.yaml)" # Nested relative placeholder
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: aggregator-other
annotations:
# Mixed content: relative descriptor placeholder embedded in a string
'example.com/mixed-placeholder': "Path before $text(./readme.md) path after"
# Absolute descriptor placeholder (should NOT be replaced):
'example.com/abs-placeholder': "Check $yaml(https://example.com/another.yaml) here"
links:
# Edge case: a link without a URL field – should be skipped by substitution logic
- title: "No URL link"
spec:
type: openapi
definition: |
openapi: "3.0.0"
info:
title: "Aggregator-Other"
version: "1.0"
import { Entity } from '@backstage/catalog-model';
const isRelativePath = (url: string): boolean =>
!/^([a-z]+:\/\/|\/|#)/i.test(url);
/**
* Rewrites relative paths to absolute URLs in a Backstage entity.
*
* @param entity - The Backstage entity to process.
* @param baseUrl - The base URL used to resolve relative paths.
* @returns A new entity with relative paths substituted.
*/
export function substituteRelativePaths(entity: Entity, baseUrl: string): Entity {
// Deep copy to avoid mutating the original entity.
const result: Entity = JSON.parse(JSON.stringify(entity));
// Process metadata.annotations if present.
if (result.metadata?.annotations) {
Object.entries(result.metadata.annotations).forEach(([key, value]) => {
if (typeof value === 'string' && isRelativePath(value)) {
result.metadata.annotations[key] = resolveUrl(baseUrl, value);
}
});
}
// Process metadata.links if present.
if (Array.isArray(result.metadata?.links)) {
result.metadata.links = result.metadata.links.map(link => {
if (link.url && typeof link.url === 'string' && isRelativePath(link.url)) {
return { ...link, url: resolveUrl(baseUrl, link.url) };
}
return link;
});
}
/**
* Recursively processes an object and substitutes descriptor placeholders
* of the form $text(...), $json(...), or $yaml(...) if they contain relative paths.
*
* @param obj - The object whose string values will be processed.
*/
function substitutePlaceholders(obj: Record<string, any>): void {
Object.keys(obj).forEach(key => {
const value = obj[key];
if (typeof value === 'string') {
// Regex to match $text(...), $json(...), or $yaml(...)
const placeholderRegex = /\$(text|json|yaml)\((.+?)\)/g;
obj[key] = value.replace(placeholderRegex, (match, type, filePath) => {
if (isRelativePath(filePath)) {
return `$${type}(${resolveUrl(baseUrl, filePath)})`;
}
return match;
});
} else if (value && typeof value === 'object') {
substitutePlaceholders(value);
}
});
}
substitutePlaceholders(result);
return result;
}
/**
* Dummy implementation of resolveUrl for demonstration purposes.
* Replace or import your actual implementation as needed.
*/
function resolveUrl(baseUrl: string, relativePath: string): string {
// A simple example: ensure there's exactly one slash between baseUrl and relativePath.
return baseUrl.replace(/\/+$/, '') + '/' + relativePath.replace(/^\/+/, '');
}
import { Entity } from '@backstage/catalog-model';
const isRelativePath = (url: string): boolean =>
!/^([a-z]+:\/\/|\/|#)/i.test(url);
/**
* Rewrites relative paths to absolute URLs in a Backstage entity.
*
* @param entity - The Backstage entity to process.
* @param baseUrl - The base URL used to resolve relative paths.
* @returns A new entity with relative paths substituted.
*/
export function substituteRelativePaths(entity: Entity, baseUrl: string): Entity {
// Deep copy to avoid mutating the original entity.
const result: Entity = JSON.parse(JSON.stringify(entity));
// Process metadata.annotations if present.
if (result.metadata?.annotations) {
Object.entries(result.metadata.annotations).forEach(([key, value]) => {
if (typeof value === 'string' && isRelativePath(value)) {
result.metadata.annotations[key] = resolveUrl(baseUrl, value);
}
});
}
// Process metadata.links if present.
if (Array.isArray(result.metadata?.links)) {
result.metadata.links = result.metadata.links.map(link => {
if (link.url && typeof link.url === 'string' && isRelativePath(link.url)) {
return { ...link, url: resolveUrl(baseUrl, link.url) };
}
return link;
});
}
/**
* Recursively processes an object and substitutes descriptor placeholders
* of the form $text(...), $json(...), or $yaml(...) if they contain relative paths.
*
* @param obj - The object whose string values will be processed.
*/
function substitutePlaceholders(obj: Record<string, any>): void {
Object.keys(obj).forEach(key => {
const value = obj[key];
if (typeof value === 'string') {
// Regex to match $text(...), $json(...), or $yaml(...)
const placeholderRegex = /\$(text|json|yaml)\((.+?)\)/g;
obj[key] = value.replace(placeholderRegex, (match, type, filePath) => {
if (isRelativePath(filePath)) {
return `$${type}(${resolveUrl(baseUrl, filePath)})`;
}
return match;
});
} else if (value && typeof value === 'object') {
substitutePlaceholders(value);
}
});
}
substitutePlaceholders(result);
return result;
}
/**
* Dummy implementation of resolveUrl for demonstration purposes.
* Replace or import your actual implementation as needed.
*/
function resolveUrl(baseUrl: string, relativePath: string): string {
// A simple example: ensure there's exactly one slash between baseUrl and relativePath.
return baseUrl.replace(/\/+$/, '') + '/' + relativePath.replace(/^\/+/, '');
}
import { Entity } from '@backstage/catalog-model';
const isRelativePath = (url: string): boolean =>
!/^([a-z]+:\/\/|\/|#)/i.test(url);
/**
* Substitutes relative URLs in descriptor placeholders for a Backstage entity.
* It recursively searches for keys '$text', '$json', and '$yaml' and, if the
* associated value is a string that represents a relative URL, substitutes it with an absolute URL based on baseUrl.
*
* This function mutates the input entity in memory.
*
* @param entity - The Backstage entity to process.
* @param baseUrl - The base URL used to resolve relative paths.
* @returns The mutated entity with substituted descriptor paths.
*/
export function substituteRelativePaths(entity: Entity, baseUrl: string): Entity {
// In-place mutation: we modify the passed entity directly.
function substituteDescriptors(obj: Record<string, any>): void {
for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
const value = obj[key];
// If the key is one of the descriptor placeholders
if (key === '$text' || key === '$json' || key === '$yaml') {
if (typeof value === 'string' && isRelativePath(value)) {
obj[key] = resolveUrl(baseUrl, value);
}
} else if (value && typeof value === 'object') {
substituteDescriptors(value);
}
}
}
substituteDescriptors(entity);
return entity;
}
/**
* Dummy implementation of resolveUrl for demonstration purposes.
* Replace with your actual URL resolution logic.
*/
function resolveUrl(baseUrl: string, relativePath: string): string {
return baseUrl.replace(/\/+$/, '') + '/' + relativePath.replace(/^\/+/, '');
}
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: petstore
description: "The Petstore API"
spec:
type: openapi
lifecycle: production
owner: [email protected]
# Descriptor placeholder as key: relative path to be substituted
definition:
$text: ./files/relative-swagger.json
extra:
data:
# Relative path that will be substituted
$json: ./files/relative-data.json
# Absolute URL remains unchanged
$yaml: https://example.com/data.yaml
---
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: my-component
description: "A component with descriptor placeholders"
spec:
owner: team-a
details:
# Relative descriptor placeholder that will be substituted
$text: ./docs/readme.md
nested:
# Another relative descriptor placeholder within a nested object
$json: ./configs/config.json
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: petstore
description: "The Petstore API"
spec:
type: openapi
lifecycle: production
owner: [email protected]
# Descriptor placeholder as key: relative path to be substituted
definition:
$text: ./files/relative-swagger.json
extra:
data:
# Relative path that will be substituted
$json: ./files/relative-data.json
# Absolute URL remains unchanged
$yaml: https://example.com/data.yaml
---
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: my-component
description: "A component with descriptor placeholders"
spec:
owner: team-a
details:
# Relative descriptor placeholder that will be substituted
$text: ./docs/readme.md
nested:
# Another relative descriptor placeholder within a nested object
$json: ./configs/config.json
import { Entity } from '@backstage/catalog-model';
const isRelativePath = (url: string): boolean =>
!/^([a-z]+:\/\/|\/|#)/i.test(url);
/**
* Recursively searches the given object for descriptor keys ('$text', '$json', '$yaml')
* and, if the associated value is a string representing a relative URL, substitutes it
* with an absolute URL using the provided baseUrl.
*
* @param obj - The object to process.
* @param baseUrl - The base URL used to resolve relative paths.
*/
function substituteDescriptorPlaceholders(obj: Record<string, any>, baseUrl: string): void {
for (const key in obj) {
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
const value = obj[key];
if (key === '$text' || key === '$json' || key === '$yaml') {
if (typeof value === 'string' && isRelativePath(value)) {
obj[key] = resolveUrl(baseUrl, value);
}
} else if (value && typeof value === 'object') {
substituteDescriptorPlaceholders(value, baseUrl);
}
}
}
/**
* Substitutes relative URLs in descriptor placeholders for a Backstage entity.
* This function directly mutates the input entity in memory.
*
* @param entity - The Backstage entity to process.
* @param baseUrl - The base URL used to resolve relative paths.
* @returns The mutated entity with substituted descriptor paths.
*/
export function substituteRelativePaths(entity: Entity, baseUrl: string): Entity {
substituteDescriptorPlaceholders(entity, baseUrl);
return entity;
}
/**
* Dummy implementation of resolveUrl for demonstration purposes.
* Replace with your actual URL resolution logic.
*
* @param baseUrl - The base URL.
* @param relativePath - The relative path to be resolved.
* @returns The absolute URL.
*/
function resolveUrl(baseUrl: string, relativePath: string): string {
return baseUrl.replace(/\/+$/, '') + '/' + relativePath.replace(/^\/+/, '');
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment