Last active
March 5, 2025 17:48
-
-
Save brandonbryant12/f232e40319a88a30b51c3cb8c98fd14d to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(/^\/+/, ''); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(/^\/+/, ''); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(/^\/+/, ''); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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