frontend-plugin-api: refactor to use opaque type for route refs
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': patch
|
||||
'@backstage/frontend-app-api': patch
|
||||
'@backstage/core-compat-api': patch
|
||||
---
|
||||
|
||||
Internal refactor of route reference implementations with minor updates to the `toString` implementations.
|
||||
@@ -34,8 +34,10 @@
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/plugin-catalog-react": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"lodash": "^4.17.21"
|
||||
"lodash": "^4.17.21",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
|
||||
@@ -31,13 +31,11 @@ import {
|
||||
createExternalRouteRef as createNewExternalRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { convertLegacyRouteRef } from './convertLegacyRouteRef';
|
||||
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalRouteRef as toInternalNewRouteRef } from '../../frontend-plugin-api/src/routing/RouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalSubRouteRef as toInternalNewSubRouteRef } from '../../frontend-plugin-api/src/routing/SubRouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExternalRouteRef as toInternalNewExternalRouteRef } from '../../frontend-plugin-api/src/routing/ExternalRouteRef';
|
||||
import {
|
||||
OpaqueExternalRouteRef,
|
||||
OpaqueRouteRef,
|
||||
OpaqueSubRouteRef,
|
||||
} from '@internal/frontend';
|
||||
|
||||
describe('convertLegacyRouteRef', () => {
|
||||
it('converts old to new', () => {
|
||||
@@ -85,13 +83,13 @@ describe('convertLegacyRouteRef', () => {
|
||||
expect(ref3).toBe(ref3Converted);
|
||||
expect(ref4).toBe(ref4Converted);
|
||||
|
||||
const ref1Internal = toInternalNewRouteRef(ref1Converted);
|
||||
const ref2Internal = toInternalNewRouteRef(ref2Converted);
|
||||
const ref1sub1Internal = toInternalNewSubRouteRef(ref1sub1Converted);
|
||||
const ref1sub2Internal = toInternalNewSubRouteRef(ref1sub2Converted);
|
||||
const ref2sub1Internal = toInternalNewSubRouteRef(ref2sub1Converted);
|
||||
const ref3Internal = toInternalNewExternalRouteRef(ref3Converted);
|
||||
const ref4Internal = toInternalNewExternalRouteRef(ref4Converted);
|
||||
const ref1Internal = OpaqueRouteRef.toInternal(ref1Converted);
|
||||
const ref2Internal = OpaqueRouteRef.toInternal(ref2Converted);
|
||||
const ref1sub1Internal = OpaqueSubRouteRef.toInternal(ref1sub1Converted);
|
||||
const ref1sub2Internal = OpaqueSubRouteRef.toInternal(ref1sub2Converted);
|
||||
const ref2sub1Internal = OpaqueSubRouteRef.toInternal(ref2sub1Converted);
|
||||
const ref3Internal = OpaqueExternalRouteRef.toInternal(ref3Converted);
|
||||
const ref4Internal = OpaqueExternalRouteRef.toInternal(ref4Converted);
|
||||
|
||||
expect(ref1Internal.getDescription()).toBe(
|
||||
'routeRef{type=absolute,id=ref1}',
|
||||
|
||||
@@ -32,13 +32,11 @@ import {
|
||||
createSubRouteRef,
|
||||
createExternalRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalRouteRef } from '../../frontend-plugin-api/src/routing/RouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalSubRouteRef } from '../../frontend-plugin-api/src/routing/SubRouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExternalRouteRef } from '../../frontend-plugin-api/src/routing/ExternalRouteRef';
|
||||
import {
|
||||
OpaqueRouteRef,
|
||||
OpaqueSubRouteRef,
|
||||
OpaqueExternalRouteRef,
|
||||
} from '@internal/frontend';
|
||||
|
||||
/**
|
||||
* Converts a legacy route ref type to the new system.
|
||||
@@ -184,7 +182,7 @@ function convertNewToOld(
|
||||
ref: RouteRef | SubRouteRef | ExternalRouteRef,
|
||||
): LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef {
|
||||
if (ref.$$type === '@backstage/RouteRef') {
|
||||
const newRef = toInternalRouteRef(ref);
|
||||
const newRef = OpaqueRouteRef.toInternal(ref);
|
||||
return Object.assign(ref, {
|
||||
[routeRefType]: 'absolute',
|
||||
params: newRef.getParams(),
|
||||
@@ -192,7 +190,7 @@ function convertNewToOld(
|
||||
} as Omit<LegacyRouteRef, '$$routeRefType'>) as unknown as LegacyRouteRef;
|
||||
}
|
||||
if (ref.$$type === '@backstage/SubRouteRef') {
|
||||
const newRef = toInternalSubRouteRef(ref);
|
||||
const newRef = OpaqueSubRouteRef.toInternal(ref);
|
||||
return Object.assign(ref, {
|
||||
[routeRefType]: 'sub',
|
||||
parent: convertLegacyRouteRef(newRef.getParent()),
|
||||
@@ -200,7 +198,7 @@ function convertNewToOld(
|
||||
} as Omit<LegacySubRouteRef, '$$routeRefType' | 'path'>) as unknown as LegacySubRouteRef;
|
||||
}
|
||||
if (ref.$$type === '@backstage/ExternalRouteRef') {
|
||||
const newRef = toInternalExternalRouteRef(ref);
|
||||
const newRef = OpaqueExternalRouteRef.toInternal(ref);
|
||||
return Object.assign(ref, {
|
||||
[routeRefType]: 'external',
|
||||
optional: true,
|
||||
@@ -221,7 +219,7 @@ function convertOldToNew(
|
||||
if (type === 'absolute') {
|
||||
const legacyRef = ref as LegacyRouteRef;
|
||||
const legacyRefStr = String(legacyRef);
|
||||
const newRef = toInternalRouteRef(
|
||||
const newRef = OpaqueRouteRef.toInternal(
|
||||
createRouteRef<{ [key in string]: string }>({
|
||||
params: legacyRef.params as string[],
|
||||
}),
|
||||
@@ -247,7 +245,7 @@ function convertOldToNew(
|
||||
if (type === 'sub') {
|
||||
const legacyRef = ref as LegacySubRouteRef;
|
||||
const legacyRefStr = String(legacyRef);
|
||||
const newRef = toInternalSubRouteRef(
|
||||
const newRef = OpaqueSubRouteRef.toInternal(
|
||||
createSubRouteRef({
|
||||
path: legacyRef.path,
|
||||
parent: convertLegacyRouteRef(legacyRef.parent),
|
||||
@@ -274,7 +272,7 @@ function convertOldToNew(
|
||||
if (type === 'external') {
|
||||
const legacyRef = ref as LegacyExternalRouteRef;
|
||||
const legacyRefStr = String(legacyRef);
|
||||
const newRef = toInternalExternalRouteRef(
|
||||
const newRef = OpaqueExternalRouteRef.toInternal(
|
||||
createExternalRouteRef<{ [key in string]: string }>({
|
||||
params: legacyRef.params as string[],
|
||||
defaultTarget:
|
||||
|
||||
@@ -16,8 +16,7 @@
|
||||
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRefsById } from './collectRouteIds';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalRouteRef } from '../../../frontend-plugin-api/src/routing/RouteRef';
|
||||
import { OpaqueRouteRef } from '@internal/frontend';
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -41,7 +40,7 @@ export function createRouteAliasResolver(
|
||||
|
||||
let currentRef = routeRef;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const alias = toInternalRouteRef(currentRef).alias;
|
||||
const alias = OpaqueRouteRef.toInternal(currentRef).alias;
|
||||
if (alias) {
|
||||
if (pluginId) {
|
||||
const [aliasPluginId] = alias.split('.');
|
||||
|
||||
@@ -25,18 +25,11 @@ import {
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import mapValues from 'lodash/mapValues';
|
||||
import { AnyRouteRef, BackstageRouteObject } from './types';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { isRouteRef } from '../../../frontend-plugin-api/src/routing/RouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
isSubRouteRef,
|
||||
toInternalSubRouteRef,
|
||||
} from '../../../frontend-plugin-api/src/routing/SubRouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
isExternalRouteRef,
|
||||
toInternalExternalRouteRef,
|
||||
} from '../../../frontend-plugin-api/src/routing/ExternalRouteRef';
|
||||
OpaqueRouteRef,
|
||||
OpaqueExternalRouteRef,
|
||||
OpaqueSubRouteRef,
|
||||
} from '@internal/frontend';
|
||||
import { RouteAliasResolver } from './RouteAliasResolver';
|
||||
|
||||
// Joins a list of paths together, avoiding trailing and duplicate slashes
|
||||
@@ -65,10 +58,10 @@ function resolveTargetRef(
|
||||
let ref: AnyRouteRef = targetRouteRef;
|
||||
let path = '';
|
||||
|
||||
if (isExternalRouteRef(ref)) {
|
||||
if (OpaqueExternalRouteRef.isType(ref)) {
|
||||
let resolvedRoute = routeBindings.get(ref);
|
||||
if (!resolvedRoute) {
|
||||
const internal = toInternalExternalRouteRef(ref);
|
||||
const internal = OpaqueExternalRouteRef.toInternal(ref);
|
||||
const defaultTarget = internal.getDefaultTarget();
|
||||
if (defaultTarget) {
|
||||
resolvedRoute = routeRefsById.get(defaultTarget);
|
||||
@@ -80,13 +73,13 @@ function resolveTargetRef(
|
||||
ref = resolvedRoute;
|
||||
}
|
||||
|
||||
if (isSubRouteRef(ref)) {
|
||||
const internal = toInternalSubRouteRef(ref);
|
||||
if (OpaqueSubRouteRef.isType(ref)) {
|
||||
const internal = OpaqueSubRouteRef.toInternal(ref);
|
||||
path = ref.path;
|
||||
ref = internal.getParent();
|
||||
}
|
||||
|
||||
if (!isRouteRef(ref)) {
|
||||
if (!OpaqueRouteRef.isType(ref)) {
|
||||
throw new Error(
|
||||
`Unexpectedly resolved ${targetRouteRef} to a non-route ref ${ref}`,
|
||||
);
|
||||
|
||||
@@ -20,16 +20,12 @@ import {
|
||||
ExternalRouteRef,
|
||||
FrontendFeature,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
isRouteRef,
|
||||
toInternalRouteRef,
|
||||
} from '../../../frontend-plugin-api/src/routing/RouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExternalRouteRef } from '../../../frontend-plugin-api/src/routing/ExternalRouteRef';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalSubRouteRef } from '../../../frontend-plugin-api/src/routing/SubRouteRef';
|
||||
import { OpaqueFrontendPlugin } from '@internal/frontend';
|
||||
OpaqueRouteRef,
|
||||
OpaqueSubRouteRef,
|
||||
OpaqueExternalRouteRef,
|
||||
OpaqueFrontendPlugin,
|
||||
} from '@internal/frontend';
|
||||
import { ErrorCollector } from '../wiring/createErrorCollector';
|
||||
|
||||
/** @internal */
|
||||
@@ -62,12 +58,12 @@ export function collectRouteIds(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isRouteRef(ref)) {
|
||||
const internalRef = toInternalRouteRef(ref);
|
||||
if (OpaqueRouteRef.isType(ref)) {
|
||||
const internalRef = OpaqueRouteRef.toInternal(ref);
|
||||
internalRef.setId(refId);
|
||||
routesById.set(refId, ref);
|
||||
} else {
|
||||
const internalRef = toInternalSubRouteRef(ref);
|
||||
const internalRef = OpaqueSubRouteRef.toInternal(ref);
|
||||
routesById.set(refId, internalRef);
|
||||
}
|
||||
}
|
||||
@@ -82,7 +78,7 @@ export function collectRouteIds(
|
||||
continue;
|
||||
}
|
||||
|
||||
const internalRef = toInternalExternalRouteRef(ref);
|
||||
const internalRef = OpaqueExternalRouteRef.toInternal(ref);
|
||||
internalRef.setId(refId);
|
||||
externalRoutesById.set(refId, ref);
|
||||
}
|
||||
|
||||
@@ -23,8 +23,7 @@ import { RouteRefsById } from './collectRouteIds';
|
||||
import { ErrorCollector } from '../wiring/createErrorCollector';
|
||||
import { Config } from '@backstage/config';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { toInternalExternalRouteRef } from '../../../frontend-plugin-api/src/routing/ExternalRouteRef';
|
||||
import { OpaqueExternalRouteRef } from '@internal/frontend';
|
||||
|
||||
/**
|
||||
* Extracts a union of the keys in a map whose value extends the given type
|
||||
@@ -166,7 +165,7 @@ export function resolveRouteBindings(
|
||||
for (const externalRef of routesById.externalRoutes.values()) {
|
||||
if (!result.has(externalRef) && !disabledExternalRefs.has(externalRef)) {
|
||||
const defaultRefId =
|
||||
toInternalExternalRouteRef(externalRef).getDefaultTarget();
|
||||
OpaqueExternalRouteRef.toInternal(externalRef).getDefaultTarget();
|
||||
if (defaultRefId) {
|
||||
const defaultRef = routesById.routes.get(defaultRefId);
|
||||
if (defaultRef) {
|
||||
|
||||
@@ -14,4 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './routing';
|
||||
export * from './wiring';
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { OpaqueType } from '@internal/opaque';
|
||||
|
||||
export const OpaqueExternalRouteRef = OpaqueType.create<{
|
||||
public: ExternalRouteRef;
|
||||
versions: {
|
||||
readonly version: 'v1';
|
||||
|
||||
getParams(): string[];
|
||||
getDescription(): string;
|
||||
getDefaultTarget(): string | undefined;
|
||||
|
||||
setId(id: string): void;
|
||||
};
|
||||
}>({
|
||||
type: '@backstage/ExternalRouteRef',
|
||||
versions: ['v1'],
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { OpaqueType } from '@internal/opaque';
|
||||
|
||||
export const OpaqueRouteRef = OpaqueType.create<{
|
||||
public: RouteRef;
|
||||
versions: {
|
||||
readonly version: 'v1';
|
||||
|
||||
getParams(): string[];
|
||||
getDescription(): string;
|
||||
|
||||
alias: string | undefined;
|
||||
|
||||
setId(id: string): void;
|
||||
};
|
||||
}>({
|
||||
type: '@backstage/RouteRef',
|
||||
versions: ['v1'],
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RouteRef, SubRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { OpaqueType } from '@internal/opaque';
|
||||
|
||||
export const OpaqueSubRouteRef = OpaqueType.create<{
|
||||
public: SubRouteRef;
|
||||
versions: {
|
||||
readonly version: 'v1';
|
||||
|
||||
getParams(): string[];
|
||||
getParent(): RouteRef;
|
||||
getDescription(): string;
|
||||
};
|
||||
}>({
|
||||
type: '@backstage/SubRouteRef',
|
||||
versions: ['v1'],
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { OpaqueRouteRef } from './OpaqueRouteRef';
|
||||
export { OpaqueSubRouteRef } from './OpaqueSubRouteRef';
|
||||
export { OpaqueExternalRouteRef } from './OpaqueExternalRouteRef';
|
||||
@@ -14,17 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ExternalRouteRef,
|
||||
createExternalRouteRef,
|
||||
toInternalExternalRouteRef,
|
||||
} from './ExternalRouteRef';
|
||||
import { ExternalRouteRef, createExternalRouteRef } from './ExternalRouteRef';
|
||||
import { OpaqueExternalRouteRef } from '@internal/frontend';
|
||||
import { AnyRouteRefParams } from './types';
|
||||
|
||||
describe('ExternalRouteRef', () => {
|
||||
it('should be created', () => {
|
||||
const routeRef: ExternalRouteRef<undefined> = createExternalRouteRef();
|
||||
const internal = toInternalExternalRouteRef(routeRef);
|
||||
const internal = OpaqueExternalRouteRef.toInternal(routeRef);
|
||||
expect(internal.getParams()).toEqual([]);
|
||||
|
||||
expect(String(internal)).toMatch(
|
||||
@@ -39,7 +36,7 @@ describe('ExternalRouteRef', () => {
|
||||
x: string;
|
||||
y: string;
|
||||
}> = createExternalRouteRef({ params: ['x', 'y'] });
|
||||
const internal = toInternalExternalRouteRef(routeRef);
|
||||
const internal = OpaqueExternalRouteRef.toInternal(routeRef);
|
||||
expect(internal.getParams()).toEqual(['x', 'y']);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RouteRefImpl } from './RouteRef';
|
||||
import { OpaqueExternalRouteRef } from '@internal/frontend';
|
||||
import { describeParentCallSite } from './describeParentCallSite';
|
||||
import { AnyRouteRefParams } from './types';
|
||||
|
||||
@@ -58,37 +58,6 @@ export function toInternalExternalRouteRef<
|
||||
return r;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isExternalRouteRef(opaque: {
|
||||
$$type: string;
|
||||
}): opaque is ExternalRouteRef {
|
||||
return opaque.$$type === '@backstage/ExternalRouteRef';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
class ExternalRouteRefImpl
|
||||
extends RouteRefImpl
|
||||
implements InternalExternalRouteRef
|
||||
{
|
||||
readonly $$type = '@backstage/ExternalRouteRef' as any;
|
||||
readonly params: string[];
|
||||
readonly defaultTarget: string | undefined;
|
||||
|
||||
constructor(
|
||||
params: string[] = [],
|
||||
defaultTarget: string | undefined,
|
||||
creationSite: string,
|
||||
) {
|
||||
super(params, creationSite);
|
||||
this.params = params;
|
||||
this.defaultTarget = defaultTarget;
|
||||
}
|
||||
|
||||
getDefaultTarget() {
|
||||
return this.defaultTarget;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a route descriptor, to be later bound to a concrete route by the app. Used to implement cross-plugin route references.
|
||||
*
|
||||
@@ -102,7 +71,7 @@ class ExternalRouteRefImpl
|
||||
export function createExternalRouteRef<
|
||||
TParams extends { [param in TParamKeys]: string } | undefined = undefined,
|
||||
TParamKeys extends string = string,
|
||||
>(options?: {
|
||||
>(config?: {
|
||||
/**
|
||||
* The parameters that will be provided to the external route reference.
|
||||
*/
|
||||
@@ -124,9 +93,38 @@ export function createExternalRouteRef<
|
||||
? TParams
|
||||
: { [param in TParamKeys]: string }
|
||||
> {
|
||||
return new ExternalRouteRefImpl(
|
||||
options?.params as string[] | undefined,
|
||||
options?.defaultTarget,
|
||||
describeParentCallSite(),
|
||||
);
|
||||
const params = (config?.params ?? []) as string[];
|
||||
const creationSite = describeParentCallSite();
|
||||
|
||||
let id: string | undefined = undefined;
|
||||
|
||||
return OpaqueExternalRouteRef.createInstance('v1', {
|
||||
T: undefined as unknown as TParams,
|
||||
getParams() {
|
||||
return params;
|
||||
},
|
||||
getDescription() {
|
||||
if (id) {
|
||||
return id;
|
||||
}
|
||||
return `created at '${creationSite}'`;
|
||||
},
|
||||
getDefaultTarget() {
|
||||
return config?.defaultTarget;
|
||||
},
|
||||
setId(newId: string) {
|
||||
if (!newId) {
|
||||
throw new Error(`ExternalRouteRef id must be a non-empty string`);
|
||||
}
|
||||
if (id && id !== newId) {
|
||||
throw new Error(
|
||||
`ExternalRouteRef was referenced twice as both '${id}' and '${newId}'`,
|
||||
);
|
||||
}
|
||||
id = newId;
|
||||
},
|
||||
toString(): string {
|
||||
return `externalRouteRef{id=${id},at='${creationSite}'}`;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
*/
|
||||
|
||||
import { AnyRouteRefParams } from './types';
|
||||
import { RouteRef, createRouteRef, toInternalRouteRef } from './RouteRef';
|
||||
import { RouteRef, createRouteRef } from './RouteRef';
|
||||
import { OpaqueRouteRef } from '@internal/frontend';
|
||||
|
||||
describe('RouteRef', () => {
|
||||
it('should be created and have a mutable ID', () => {
|
||||
const routeRef: RouteRef<undefined> = createRouteRef();
|
||||
const internal = toInternalRouteRef(routeRef);
|
||||
const internal = OpaqueRouteRef.toInternal(routeRef);
|
||||
expect(internal.T).toBe(undefined);
|
||||
expect(internal.getParams()).toEqual([]);
|
||||
expect(internal.getDescription()).toMatch(/RouteRef\.test\.ts/);
|
||||
@@ -49,7 +50,7 @@ describe('RouteRef', () => {
|
||||
}> = createRouteRef({
|
||||
params: ['x', 'y'],
|
||||
});
|
||||
const internal = toInternalRouteRef(routeRef);
|
||||
const internal = OpaqueRouteRef.toInternal(routeRef);
|
||||
expect(internal.getParams()).toEqual(['x', 'y']);
|
||||
expect(internal.getDescription()).toMatch(/RouteRef\.test\.ts/);
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { OpaqueRouteRef } from '@internal/frontend';
|
||||
import { describeParentCallSite } from './describeParentCallSite';
|
||||
import { AnyRouteRefParams } from './types';
|
||||
|
||||
@@ -33,93 +34,6 @@ export interface RouteRef<
|
||||
readonly T: TParams;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export interface InternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
> extends RouteRef<TParams> {
|
||||
readonly version: 'v1';
|
||||
getParams(): string[];
|
||||
getDescription(): string;
|
||||
|
||||
alias: string | undefined;
|
||||
|
||||
setId(id: string): void;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
>(resource: RouteRef<TParams>): InternalRouteRef<TParams> {
|
||||
const r = resource as InternalRouteRef<TParams>;
|
||||
if (r.$$type !== '@backstage/RouteRef') {
|
||||
throw new Error(`Invalid RouteRef, bad type '${r.$$type}'`);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isRouteRef(opaque: { $$type: string }): opaque is RouteRef {
|
||||
return opaque.$$type === '@backstage/RouteRef';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class RouteRefImpl implements InternalRouteRef {
|
||||
readonly $$type = '@backstage/RouteRef';
|
||||
readonly version = 'v1';
|
||||
declare readonly T: never;
|
||||
|
||||
#id?: string;
|
||||
readonly #params: string[];
|
||||
readonly #creationSite: string;
|
||||
readonly #alias?: string;
|
||||
|
||||
constructor(
|
||||
readonly params: string[] = [],
|
||||
creationSite: string,
|
||||
alias?: string,
|
||||
) {
|
||||
this.#params = params;
|
||||
this.#creationSite = creationSite;
|
||||
this.#alias = alias;
|
||||
}
|
||||
|
||||
getParams(): string[] {
|
||||
return this.#params;
|
||||
}
|
||||
|
||||
get alias(): string | undefined {
|
||||
return this.#alias;
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
if (this.#id) {
|
||||
return this.#id;
|
||||
}
|
||||
return `created at '${this.#creationSite}'`;
|
||||
}
|
||||
|
||||
get #name() {
|
||||
return this.$$type.slice('@backstage/'.length);
|
||||
}
|
||||
|
||||
setId(id: string): void {
|
||||
if (!id) {
|
||||
throw new Error(`${this.#name} id must be a non-empty string`);
|
||||
}
|
||||
if (this.#id && this.#id !== id) {
|
||||
throw new Error(
|
||||
`${this.#name} was referenced twice as both '${this.#id}' and '${id}'`,
|
||||
);
|
||||
}
|
||||
this.#id = id;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.#name}{${this.getDescription()}}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link RouteRef} from a route descriptor.
|
||||
*
|
||||
@@ -145,9 +59,36 @@ export function createRouteRef<
|
||||
? TParams
|
||||
: { [param in TParamKeys]: string }
|
||||
> {
|
||||
return new RouteRefImpl(
|
||||
config?.params as string[] | undefined,
|
||||
describeParentCallSite(),
|
||||
config?.aliasFor,
|
||||
) as RouteRef<any>;
|
||||
const params = (config?.params ?? []) as string[];
|
||||
const creationSite = describeParentCallSite();
|
||||
|
||||
let id: string | undefined = undefined;
|
||||
|
||||
return OpaqueRouteRef.createInstance('v1', {
|
||||
T: undefined as unknown as TParams,
|
||||
getParams() {
|
||||
return params;
|
||||
},
|
||||
getDescription() {
|
||||
if (id) {
|
||||
return id;
|
||||
}
|
||||
return `created at '${creationSite}'`;
|
||||
},
|
||||
alias: config?.aliasFor,
|
||||
setId(newId: string) {
|
||||
if (!newId) {
|
||||
throw new Error(`RouteRef id must be a non-empty string`);
|
||||
}
|
||||
if (id && id !== newId) {
|
||||
throw new Error(
|
||||
`RouteRef was referenced twice as both '${id}' and '${newId}'`,
|
||||
);
|
||||
}
|
||||
id = newId;
|
||||
},
|
||||
toString(): string {
|
||||
return `routeRef{id=${id},at='${creationSite}'}`;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,24 +15,21 @@
|
||||
*/
|
||||
|
||||
import { AnyRouteRefParams } from './types';
|
||||
import {
|
||||
SubRouteRef,
|
||||
createSubRouteRef,
|
||||
toInternalSubRouteRef,
|
||||
} from './SubRouteRef';
|
||||
import { createRouteRef, toInternalRouteRef } from './RouteRef';
|
||||
import { SubRouteRef, createSubRouteRef } from './SubRouteRef';
|
||||
import { createRouteRef } from './RouteRef';
|
||||
import { OpaqueRouteRef, OpaqueSubRouteRef } from '@internal/frontend';
|
||||
|
||||
const parent = createRouteRef();
|
||||
const parentX = createRouteRef({ params: ['x'] });
|
||||
|
||||
describe('SubRouteRef', () => {
|
||||
it('should be created', () => {
|
||||
const internalParent = toInternalRouteRef(createRouteRef());
|
||||
const internalParent = OpaqueRouteRef.toInternal(createRouteRef());
|
||||
const routeRef: SubRouteRef = createSubRouteRef({
|
||||
parent: internalParent,
|
||||
path: '/foo',
|
||||
});
|
||||
const internal = toInternalSubRouteRef(routeRef);
|
||||
const internal = OpaqueSubRouteRef.toInternal(routeRef);
|
||||
expect(internal.path).toBe('/foo');
|
||||
expect(internal.T).toBe(undefined);
|
||||
expect(internal.getParent()).toBe(internalParent);
|
||||
@@ -49,7 +46,7 @@ describe('SubRouteRef', () => {
|
||||
parent,
|
||||
path: '/foo/:bar',
|
||||
});
|
||||
const internal = toInternalSubRouteRef(routeRef);
|
||||
const internal = OpaqueSubRouteRef.toInternal(routeRef);
|
||||
expect(internal.path).toBe('/foo/:bar');
|
||||
expect(internal.getParent()).toBe(parent);
|
||||
expect(internal.getParams()).toEqual(['bar']);
|
||||
@@ -64,7 +61,7 @@ describe('SubRouteRef', () => {
|
||||
parent: parentX,
|
||||
path: '/foo/:y/:z',
|
||||
});
|
||||
const internal = toInternalSubRouteRef(routeRef);
|
||||
const internal = OpaqueSubRouteRef.toInternal(routeRef);
|
||||
expect(internal.path).toBe('/foo/:y/:z');
|
||||
expect(internal.getParent()).toBe(parentX);
|
||||
expect(internal.getParams()).toEqual(['x', 'y', 'z']);
|
||||
@@ -75,7 +72,7 @@ describe('SubRouteRef', () => {
|
||||
parent: parentX,
|
||||
path: '/foo/bar',
|
||||
});
|
||||
const internal = toInternalSubRouteRef(routeRef);
|
||||
const internal = OpaqueSubRouteRef.toInternal(routeRef);
|
||||
expect(internal.path).toBe('/foo/bar');
|
||||
expect(internal.getParent()).toBe(parentX);
|
||||
expect(internal.getParams()).toEqual(['x']);
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RouteRef, toInternalRouteRef } from './RouteRef';
|
||||
import { OpaqueRouteRef, OpaqueSubRouteRef } from '@internal/frontend';
|
||||
import { RouteRef } from './RouteRef';
|
||||
import { AnyRouteRefParams } from './types';
|
||||
|
||||
// Should match the pattern in react-router
|
||||
@@ -50,59 +51,6 @@ export interface InternalSubRouteRef<
|
||||
getDescription(): string;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function toInternalSubRouteRef<
|
||||
TParams extends AnyRouteRefParams = AnyRouteRefParams,
|
||||
>(resource: SubRouteRef<TParams>): InternalSubRouteRef<TParams> {
|
||||
const r = resource as InternalSubRouteRef<TParams>;
|
||||
if (r.$$type !== '@backstage/SubRouteRef') {
|
||||
throw new Error(`Invalid SubRouteRef, bad type '${r.$$type}'`);
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function isSubRouteRef(opaque: {
|
||||
$$type: string;
|
||||
}): opaque is SubRouteRef {
|
||||
return opaque.$$type === '@backstage/SubRouteRef';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export class SubRouteRefImpl<TParams extends AnyRouteRefParams>
|
||||
implements SubRouteRef<TParams>
|
||||
{
|
||||
readonly $$type = '@backstage/SubRouteRef';
|
||||
readonly version = 'v1';
|
||||
declare readonly T: never;
|
||||
|
||||
#params: string[];
|
||||
#parent: RouteRef;
|
||||
|
||||
constructor(readonly path: string, params: string[], parent: RouteRef) {
|
||||
this.#params = params;
|
||||
this.#parent = parent;
|
||||
}
|
||||
|
||||
getParams(): string[] {
|
||||
return this.#params;
|
||||
}
|
||||
|
||||
getParent(): RouteRef {
|
||||
return this.#parent;
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
const parent = toInternalRouteRef(this.#parent);
|
||||
return `at ${this.path} with parent ${parent.getDescription()}`;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `SubRouteRef{${this.getDescription()}}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in {@link PathParams} type declaration.
|
||||
* @ignore
|
||||
@@ -168,8 +116,9 @@ export function createSubRouteRef<
|
||||
const { path, parent } = config;
|
||||
type Params = PathParams<Path>;
|
||||
|
||||
const internalParent = toInternalRouteRef(parent);
|
||||
const internalParent = OpaqueRouteRef.toInternal(parent);
|
||||
const parentParams = internalParent.getParams();
|
||||
const parentDescription = internalParent.getDescription();
|
||||
|
||||
// Collect runtime parameters from the path, e.g. ['bar', 'baz'] from '/foo/:bar/:baz'
|
||||
const pathParams = path
|
||||
@@ -195,14 +144,22 @@ export function createSubRouteRef<
|
||||
}
|
||||
}
|
||||
|
||||
// We ensure that the type of the return type is sane here
|
||||
const subRouteRef = new SubRouteRefImpl(
|
||||
return OpaqueSubRouteRef.createInstance('v1', {
|
||||
T: undefined as unknown as TrimEmptyParams<
|
||||
MergeParams<Params, ParentParams>
|
||||
>,
|
||||
path,
|
||||
params as string[],
|
||||
parent,
|
||||
) as SubRouteRef<TrimEmptyParams<MergeParams<Params, ParentParams>>>;
|
||||
|
||||
// But skip type checking of the return value itself, because the conditional
|
||||
// type checking of the parent parameter overlap is tricky to express.
|
||||
return subRouteRef as any;
|
||||
getParams() {
|
||||
return params;
|
||||
},
|
||||
getParent() {
|
||||
return parent;
|
||||
},
|
||||
getDescription() {
|
||||
return `at ${path} with parent ${parentDescription}`;
|
||||
},
|
||||
toString() {
|
||||
return `subRouteRef{path='${path}',parent=${parent}}`;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user