Merge branch 'master' of github.com:backstage/backstage into github-installations-limit

This commit is contained in:
Brian Fletcher
2021-08-09 14:53:17 +01:00
388 changed files with 5078 additions and 4982 deletions
+1
View File
@@ -61,6 +61,7 @@
},
"devDependencies": {
"@backstage/test-utils": "^0.1.16",
"@rjsf/core": "^3.0.0",
"@testing-library/cypress": "^7.0.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
+20 -4
View File
@@ -41,7 +41,15 @@ import { GcpProjectsPage } from '@backstage/plugin-gcp-projects';
import { GraphiQLPage } from '@backstage/plugin-graphiql';
import { LighthousePage } from '@backstage/plugin-lighthouse';
import { NewRelicPage } from '@backstage/plugin-newrelic';
import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder';
import {
ScaffolderPage,
scaffolderPlugin,
ScaffolderFieldExtensions,
RepoUrlPickerFieldExtension,
OwnerPickerFieldExtension,
EntityPickerFieldExtension,
EntityNamePickerFieldExtension,
} from '@backstage/plugin-scaffolder';
import { SearchPage } from '@backstage/plugin-search';
import { TechRadarPage } from '@backstage/plugin-tech-radar';
import { TechdocsPage } from '@backstage/plugin-techdocs';
@@ -54,6 +62,7 @@ import { apis } from './apis';
import { Root } from './components/Root';
import { entityPage } from './components/catalog/EntityPage';
import { searchPage } from './components/search/SearchPage';
import { LowerCaseValuePickerFieldExtension } from './components/scaffolder/customScaffolderExtensions';
import { providers } from './identityProviders';
import * as plugins from './plugins';
@@ -98,7 +107,7 @@ const AppRouter = app.getRouter();
const routes = (
<FlatRoutes>
<Navigate key="/" to="/catalog" />
<Navigate key="/" to="catalog" />
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route
path="/catalog/:namespace/:kind/:name"
@@ -108,7 +117,15 @@ const routes = (
</Route>
<Route path="/catalog-import" element={<CatalogImportPage />} />
<Route path="/docs" element={<TechdocsPage />} />
<Route path="/create" element={<ScaffolderPage />} />
<Route path="/create" element={<ScaffolderPage />}>
<ScaffolderFieldExtensions>
<EntityPickerFieldExtension />
<EntityNamePickerFieldExtension />
<RepoUrlPickerFieldExtension />
<OwnerPickerFieldExtension />
<LowerCaseValuePickerFieldExtension />
</ScaffolderFieldExtensions>
</Route>
<Route path="/explore" element={<ExplorePage />} />
<Route
path="/tech-radar"
@@ -116,7 +133,6 @@ const routes = (
/>
<Route path="/graphiql" element={<GraphiQLPage />} />
<Route path="/lighthouse" element={<LighthousePage />} />
<Route path="/api-docs" element={<ApiExplorerPage />} />
<Route path="/gcp-projects" element={<GcpProjectsPage />} />
<Route path="/newrelic" element={<NewRelicPage />} />
+1 -1
View File
@@ -82,7 +82,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarSearch />
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="/catalog" text="Home" />
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
<SidebarItem icon={LayersIcon} to="explore" text="Explore" />
@@ -0,0 +1,34 @@
/*
* Copyright 2021 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 type { FieldValidation } from '@rjsf/core';
import {
createScaffolderFieldExtension,
TextValuePicker,
scaffolderPlugin,
} from '@backstage/plugin-scaffolder';
export const LowerCaseValuePickerFieldExtension = scaffolderPlugin.provide(
createScaffolderFieldExtension({
name: 'LowerCaseValuePicker',
component: TextValuePicker,
validation: (value: string, validation: FieldValidation) => {
if (value.toLowerCase() !== value) {
validation.addError('Only lowercase values are allowed.');
}
},
}),
);
+4 -4
View File
@@ -104,7 +104,7 @@ describe('CacheManager', () => {
manager.forPlugin(plugin2Id).getClient({ defaultTtl: expectedTtl });
const client = DefaultCacheClient as jest.Mock;
const cache = (Keyv as unknown) as jest.Mock;
const cache = Keyv as unknown as jest.Mock;
expect(cache).toHaveBeenCalledTimes(2);
expect(client).toHaveBeenCalledTimes(2);
@@ -124,7 +124,7 @@ describe('CacheManager', () => {
const expectedNamespace = 'test-plugin';
manager.forPlugin(expectedNamespace).getClient();
const cache = (Keyv as unknown) as jest.Mock;
const cache = Keyv as unknown as jest.Mock;
const mockCalls = cache.mock.calls.splice(-1);
const callArgs = mockCalls[0];
expect(callArgs[0].store).toBeInstanceOf(NoStore);
@@ -138,7 +138,7 @@ describe('CacheManager', () => {
.forPlugin(expectedNamespace)
.getClient({ defaultTtl: expectedTtl });
const cache = (Keyv as unknown) as jest.Mock;
const cache = Keyv as unknown as jest.Mock;
const mockCalls = cache.mock.calls.splice(-1);
const callArgs = mockCalls[0];
expect(callArgs[0]).toMatchObject({
@@ -162,7 +162,7 @@ describe('CacheManager', () => {
const expectedTtl = 3600;
manager.forPlugin('test').getClient({ defaultTtl: expectedTtl });
const cache = (Keyv as unknown) as jest.Mock;
const cache = Keyv as unknown as jest.Mock;
const mockCacheCalls = cache.mock.calls.splice(-1);
expect(mockCacheCalls[0][0]).toMatchObject({
ttl: expectedTtl,
@@ -110,9 +110,7 @@ export class DatabaseManager {
* @returns Object with client type returned as `client` and boolean representing whether
* or not the client was overridden as `overridden`
*/
private getClientType(
pluginId: string,
): {
private getClientType(pluginId: string): {
client: string;
overridden: boolean;
} {
@@ -149,7 +149,7 @@ export async function ensureMysqlDatabaseExists(
) {
const admin = createMysqlDatabaseClient(dbConfig, {
connection: {
database: (null as unknown) as string,
database: null as unknown as string,
},
});
@@ -78,21 +78,17 @@ describe('AzureUrlReader', () => {
it.each([
{
url:
'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
config: createConfig(),
response: expect.objectContaining({
url:
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
}),
},
{
url:
'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
config: createConfig(),
response: expect.objectContaining({
url:
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
}),
},
{
@@ -129,7 +129,7 @@ export class AzureUrlReader implements UrlReader {
}
return await this.deps.treeResponseFactory.fromZipArchive({
stream: (archiveAzureResponse.body as unknown) as Readable,
stream: archiveAzureResponse.body as unknown as Readable,
etag: commitSha,
filter: options?.filter,
});
@@ -60,13 +60,8 @@ export class BitbucketUrlReader implements UrlReader {
private readonly integration: BitbucketIntegration,
private readonly deps: { treeResponseFactory: ReadTreeResponseFactory },
) {
const {
host,
apiBaseUrl,
token,
username,
appPassword,
} = integration.config;
const { host, apiBaseUrl, token, username, appPassword } =
integration.config;
if (!apiBaseUrl) {
throw new Error(
@@ -138,7 +133,7 @@ export class BitbucketUrlReader implements UrlReader {
}
return await this.deps.treeResponseFactory.fromTarArchive({
stream: (archiveBitbucketResponse.body as unknown) as Readable,
stream: archiveBitbucketResponse.body as unknown as Readable,
subpath: filepath,
etag: lastCommitShortHash,
filter: options?.filter,
@@ -41,9 +41,9 @@ const treeResponseFactory = DefaultReadTreeResponseFactory.create({
config: new ConfigReader({}),
});
const mockCredentialsProvider = ({
const mockCredentialsProvider = {
getCredentials: jest.fn().mockResolvedValue({ headers: {} }),
} as unknown) as GithubCredentialsProvider;
} as unknown as GithubCredentialsProvider;
const githubProcessor = new GithubUrlReader(
new GitHubIntegration(
@@ -39,10 +39,14 @@ import {
ReadUrlResponse,
} from './types';
export type GhRepoResponse = RestEndpointMethodTypes['repos']['get']['response']['data'];
export type GhBranchResponse = RestEndpointMethodTypes['repos']['getBranch']['response']['data'];
export type GhTreeResponse = RestEndpointMethodTypes['git']['getTree']['response']['data'];
export type GhBlobResponse = RestEndpointMethodTypes['git']['getBlob']['response']['data'];
export type GhRepoResponse =
RestEndpointMethodTypes['repos']['get']['response']['data'];
export type GhBranchResponse =
RestEndpointMethodTypes['repos']['getBranch']['response']['data'];
export type GhTreeResponse =
RestEndpointMethodTypes['git']['getTree']['response']['data'];
export type GhBlobResponse =
RestEndpointMethodTypes['git']['getBlob']['response']['data'];
/**
* A processor that adds the ability to read files from GitHub v3 APIs, such as
@@ -195,7 +199,7 @@ export class GithubUrlReader implements UrlReader {
return await this.deps.treeResponseFactory.fromTarArchive({
// TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want
// to stick to using that in exclusively backend code.
stream: (archive.body as unknown) as Readable,
stream: archive.body as unknown as Readable,
subpath,
etag: sha,
filter: options?.filter,
@@ -258,9 +262,7 @@ export class GithubUrlReader implements UrlReader {
}));
}
private async getRepoDetails(
url: string,
): Promise<{
private async getRepoDetails(url: string): Promise<{
repo: GhRepoResponse;
branch: GhBranchResponse;
}> {
@@ -108,36 +108,30 @@ describe('GitlabUrlReader', () => {
it.each([
// Project URLs
{
url:
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
url: 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
config: createConfig(),
response: expect.objectContaining({
url:
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
url: 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
headers: expect.objectContaining({
'private-token': '',
}),
}),
},
{
url:
'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
url: 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml',
config: createConfig('0123456789'),
response: expect.objectContaining({
url:
'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
url: 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
headers: expect.objectContaining({
'private-token': '0123456789',
}),
}),
},
{
url:
'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup
url: 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup
config: createConfig(),
response: expect.objectContaining({
url:
'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
url: 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch',
}),
},
@@ -170,7 +170,7 @@ export class GitlabUrlReader implements UrlReader {
}
return await this.deps.treeResponseFactory.fromTarArchive({
stream: (archiveGitLabResponse.body as unknown) as Readable,
stream: archiveGitLabResponse.body as unknown as Readable,
subpath: filepath,
etag: commitSha,
filter: options?.filter,
@@ -28,7 +28,7 @@ import {
import { stripFirstDirectoryFromPath } from './util';
// Tar types for `Parse` is not a proper constructor, but it should be
const TarParseStream = (Parse as unknown) as { new (): ParseStream };
const TarParseStream = Parse as unknown as { new (): ParseStream };
const pipeline = promisify(pipelineCb);
+9 -9
View File
@@ -123,9 +123,9 @@ describe('Git', () => {
await git.clone({ url, dir });
const { onAuth } = ((isomorphic.clone as unknown) as jest.Mock<
typeof isomorphic['clone']
>).mock.calls[0][0]!;
const { onAuth } = (
isomorphic.clone as unknown as jest.Mock<typeof isomorphic['clone']>
).mock.calls[0][0]!;
expect(onAuth()).toEqual(auth);
});
@@ -190,9 +190,9 @@ describe('Git', () => {
await git.fetch({ remote, dir });
const { onAuth } = ((isomorphic.fetch as unknown) as jest.Mock<
typeof isomorphic['fetch']
>).mock.calls[0][0]!;
const { onAuth } = (
isomorphic.fetch as unknown as jest.Mock<typeof isomorphic['fetch']>
).mock.calls[0][0]!;
expect(onAuth()).toEqual(auth);
});
@@ -279,9 +279,9 @@ describe('Git', () => {
await git.push({ remote, dir });
const { onAuth } = ((isomorphic.push as unknown) as jest.Mock<
typeof isomorphic['push']
>).mock.calls[0][0]!;
const { onAuth } = (
isomorphic.push as unknown as jest.Mock<typeof isomorphic['push']>
).mock.calls[0][0]!;
expect(onAuth()).toEqual(auth);
});
@@ -154,14 +154,8 @@ export class ServiceBuilderImpl implements ServiceBuilder {
async start(): Promise<http.Server> {
const app = express();
const {
port,
host,
logger,
corsOptions,
httpsSettings,
helmetOptions,
} = this.getOptions();
const { port, host, logger, corsOptions, httpsSettings, helmetOptions } =
this.getOptions();
app.use(helmet(helmetOptions));
if (corsOptions) {
@@ -92,18 +92,17 @@ export class DockerContainerRunner implements ContainerRunner {
Env.push(`${key}=${value}`);
}
const [
{ Error: error, StatusCode: statusCode },
] = await this.dockerClient.run(imageName, args, logStream, {
Volumes,
HostConfig: {
Binds,
},
...(workingDir ? { WorkingDir: workingDir } : {}),
Entrypoint: command,
Env,
...userOptions,
} as Docker.ContainerCreateOptions);
const [{ Error: error, StatusCode: statusCode }] =
await this.dockerClient.run(imageName, args, logStream, {
Volumes,
HostConfig: {
Binds,
},
...(workingDir ? { WorkingDir: workingDir } : {}),
Entrypoint: command,
Env,
...userOptions,
} as Docker.ContainerCreateOptions);
if (error) {
throw new Error(
@@ -38,33 +38,31 @@ export type Instance = {
databaseManager: DatabaseManager;
connections: Array<Knex>;
};
export const allDatabases: Record<
TestDatabaseId,
TestDatabaseProperties
> = Object.freeze({
POSTGRES_13: {
name: 'Postgres 13.x',
driver: 'pg',
dockerImageName: 'postgres:13',
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING',
},
POSTGRES_9: {
name: 'Postgres 9.x',
driver: 'pg',
dockerImageName: 'postgres:9',
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING',
},
MYSQL_8: {
name: 'MySQL 8.x',
driver: 'mysql2',
dockerImageName: 'mysql:8',
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING',
},
SQLITE_3: {
name: 'SQLite 3.x',
driver: 'sqlite3',
},
});
export const allDatabases: Record<TestDatabaseId, TestDatabaseProperties> =
Object.freeze({
POSTGRES_13: {
name: 'Postgres 13.x',
driver: 'pg',
dockerImageName: 'postgres:13',
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING',
},
POSTGRES_9: {
name: 'Postgres 9.x',
driver: 'pg',
dockerImageName: 'postgres:9',
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING',
},
MYSQL_8: {
name: 'MySQL 8.x',
driver: 'mysql2',
dockerImageName: 'mysql:8',
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING',
},
SQLITE_3: {
name: 'SQLite 3.x',
driver: 'sqlite3',
},
});
+3 -7
View File
@@ -190,7 +190,7 @@ export type EntityEnvelope = {
//
// @public
export function entityEnvelopeSchemaValidator<
T extends EntityEnvelope = EntityEnvelope
T extends EntityEnvelope = EntityEnvelope,
>(schema?: unknown): (data: unknown) => T;
// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen
@@ -333,9 +333,7 @@ export function getEntityName(entity: Entity): EntityName;
// Warning: (ae-missing-release-tag) "getEntitySourceLocation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export function getEntitySourceLocation(
entity: Entity,
): {
export function getEntitySourceLocation(entity: Entity): {
type: string;
target: string;
};
@@ -542,9 +540,7 @@ export function parseEntityRef(
// Warning: (ae-missing-release-tag) "parseLocationReference" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public
export function parseLocationReference(
ref: string,
): {
export function parseLocationReference(ref: string): {
type: string;
target: string;
};
@@ -58,7 +58,7 @@ describe('SchemaValidEntityPolicy', () => {
//
it('rejects wrong root type', async () => {
await expect(policy.enforce((7 as unknown) as Entity)).rejects.toThrow(
await expect(policy.enforce(7 as unknown as Entity)).rejects.toThrow(
/object/,
);
});
+1 -3
View File
@@ -18,9 +18,7 @@ import { EntityName, EntityRef } from '../types';
import { ENTITY_DEFAULT_NAMESPACE } from './constants';
import { Entity } from './Entity';
function parseRefString(
ref: string,
): {
function parseRefString(ref: string): {
kind?: string;
namespace?: string;
name: string;
@@ -30,6 +30,5 @@ export interface ApiEntityV1alpha1 extends Entity {
};
}
export const apiEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
schema,
);
export const apiEntityV1alpha1Validator =
ajvCompiledJsonSchemaValidator(schema);
@@ -33,6 +33,5 @@ export interface ComponentEntityV1alpha1 extends Entity {
};
}
export const componentEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
schema,
);
export const componentEntityV1alpha1Validator =
ajvCompiledJsonSchemaValidator(schema);
@@ -26,6 +26,5 @@ export interface DomainEntityV1alpha1 extends Entity {
};
}
export const domainEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
schema,
);
export const domainEntityV1alpha1Validator =
ajvCompiledJsonSchemaValidator(schema);
@@ -34,6 +34,5 @@ export interface GroupEntityV1alpha1 extends Entity {
};
}
export const groupEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
schema,
);
export const groupEntityV1alpha1Validator =
ajvCompiledJsonSchemaValidator(schema);
@@ -28,6 +28,5 @@ export interface LocationEntityV1alpha1 extends Entity {
};
}
export const locationEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
schema,
);
export const locationEntityV1alpha1Validator =
ajvCompiledJsonSchemaValidator(schema);
@@ -29,6 +29,5 @@ export interface ResourceEntityV1alpha1 extends Entity {
};
}
export const resourceEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
schema,
);
export const resourceEntityV1alpha1Validator =
ajvCompiledJsonSchemaValidator(schema);
@@ -27,6 +27,5 @@ export interface SystemEntityV1alpha1 extends Entity {
};
}
export const systemEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
schema,
);
export const systemEntityV1alpha1Validator =
ajvCompiledJsonSchemaValidator(schema);
@@ -40,6 +40,5 @@ export interface TemplateEntityV1beta2 extends Entity {
};
}
export const templateEntityV1beta2Validator = ajvCompiledJsonSchemaValidator(
schema,
);
export const templateEntityV1beta2Validator =
ajvCompiledJsonSchemaValidator(schema);
@@ -31,6 +31,5 @@ export interface UserEntityV1alpha1 extends Entity {
};
}
export const userEntityV1alpha1Validator = ajvCompiledJsonSchemaValidator(
schema,
);
export const userEntityV1alpha1Validator =
ajvCompiledJsonSchemaValidator(schema);
@@ -26,9 +26,10 @@ import { LOCATION_ANNOTATION, SOURCE_LOCATION_ANNOTATION } from './annotation';
* @param ref A string-form location reference, e.g. 'url:https://host'
* @returns A location reference, e.g. { type: 'url', target: 'https://host' }
*/
export function parseLocationReference(
ref: string,
): { type: string; target: string } {
export function parseLocationReference(ref: string): {
type: string;
target: string;
} {
if (typeof ref !== 'string') {
throw new TypeError(
`Unable to parse location reference '${ref}', unexpected argument ${typeof ref}`,
@@ -91,9 +92,10 @@ export function stringifyLocationReference(ref: {
* using the UrlReader from @backstage/backend-common. If it is not of type 'url', the caller
* needs to have explicit handling of each location type or signal that it is not supported.
*/
export function getEntitySourceLocation(
entity: Entity,
): { type: string; target: string } {
export function getEntitySourceLocation(entity: Entity): {
type: string;
target: string;
} {
const locationRef =
entity.metadata?.annotations?.[SOURCE_LOCATION_ANNOTATION] ??
entity.metadata?.annotations?.[LOCATION_ANNOTATION];
@@ -41,7 +41,7 @@ import { compileAjvSchema, throwAjvError } from './ajv';
* @see https://github.com/backstage/backstage/tree/master/packages/catalog-model/src/schema
*/
export function entityEnvelopeSchemaValidator<
T extends EntityEnvelope = EntityEnvelope
T extends EntityEnvelope = EntityEnvelope,
>(schema?: unknown): (data: unknown) => T {
const validate = compileAjvSchema(
schema ? (schema as Schema) : entityEnvelopeSchema,
+15 -14
View File
@@ -50,9 +50,7 @@
"@svgr/plugin-svgo": "5.4.x",
"@svgr/rollup": "5.5.x",
"@svgr/webpack": "5.5.x",
"@types/start-server-webpack-plugin": "^2.2.0",
"@types/webpack-env": "^1.15.2",
"@types/webpack-node-externals": "^2.5.0",
"@typescript-eslint/eslint-plugin": "^v4.28.3",
"@typescript-eslint/parser": "^v4.28.3",
"@yarnpkg/lockfile": "^1.1.0",
@@ -76,18 +74,19 @@
"eslint-plugin-react-hooks": "^4.0.0",
"express": "^4.17.1",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^6.2.9",
"fork-ts-checker-webpack-plugin": "^4.0.5",
"fs-extra": "9.1.0",
"handlebars": "^4.7.3",
"html-webpack-plugin": "^4.3.0",
"html-webpack-plugin": "^5.3.1",
"inquirer": "^7.0.4",
"jest": "^26.0.1",
"jest-css-modules": "^2.1.0",
"json-schema": "^0.3.0",
"lodash": "^4.17.19",
"mini-css-extract-plugin": "^0.9.0",
"mini-css-extract-plugin": "^1.4.1",
"ora": "^5.3.0",
"postcss": "^8.1.0",
"process": "^0.11.10",
"raw-loader": "^4.0.1",
"react": "^16.0.0",
"react-dev-utils": "^11.0.4",
@@ -100,17 +99,18 @@
"rollup-plugin-peer-deps-external": "^2.2.2",
"rollup-plugin-postcss": "^4.0.0",
"rollup-pluginutils": "^2.8.2",
"run-script-webpack-plugin": "^0.0.11",
"semver": "^7.3.2",
"start-server-webpack-plugin": "^2.2.5",
"style-loader": "^1.2.1",
"sucrase": "^3.18.2",
"tar": "^6.1.2",
"terser-webpack-plugin": "^1.4.3",
"terser-webpack-plugin": "^5.1.3",
"ts-loader": "^8.0.17",
"typescript": "^4.0.3",
"url-loader": "^4.1.0",
"webpack": "^4.41.6",
"webpack-dev-server": "3.11.0",
"util": "^0.12.3",
"webpack": "^5.48.0",
"webpack-dev-server": "4.0.0-rc.0",
"webpack-node-externals": "^3.0.0",
"yaml": "^1.10.0",
"yaml-jest": "^1.0.5",
@@ -129,25 +129,26 @@
"@types/diff": "^5.0.0",
"@types/express": "^4.17.6",
"@types/fs-extra": "^9.0.1",
"@types/html-webpack-plugin": "^3.2.2",
"@types/http-proxy": "^1.17.4",
"@types/inquirer": "^7.3.1",
"@types/mini-css-extract-plugin": "^1.2.2",
"@types/mock-fs": "^4.13.0",
"@types/node": "^14.14.32",
"@types/react-dev-utils": "^9.0.4",
"@types/recursive-readdir": "^2.2.0",
"@types/rollup-plugin-peer-deps-external": "^2.2.0",
"@types/rollup-plugin-postcss": "^2.0.0",
"@types/tar": "^4.0.3",
"@types/webpack": "^4.41.7",
"@types/webpack-dev-server": "^3.11.0",
"@types/terser-webpack-plugin": "^5.0.4",
"@types/webpack": "^5.28.0",
"@types/webpack-dev-server": "^3.11.5",
"@types/yarnpkg__lockfile": "^1.1.4",
"del": "^6.0.0",
"mock-fs": "^4.13.0",
"nodemon": "^2.0.2",
"ts-node": "^10.0.0"
},
"resolutions": {
"@types/webpack-dev-server/@types/webpack": "^5.28.0"
},
"files": [
"asset-types",
"templates",
+3 -5
View File
@@ -55,11 +55,9 @@ export default async (cmd: Command) => {
lockfile.replaceVersions(result.newVersions);
await lockfile.save();
} else {
const [
newVersionsForbidden,
newVersionsAllowed,
] = partition(result.newVersions, ({ name }) =>
forbiddenDuplicatesFilter(name),
const [newVersionsForbidden, newVersionsAllowed] = partition(
result.newVersions,
({ name }) => forbiddenDuplicatesFilter(name),
);
if (newVersionsForbidden.length && !fix) {
success = false;
@@ -15,14 +15,14 @@
*/
import { resolve as resolvePath } from 'path';
import { ResolvePlugin } from 'webpack';
import { WebpackPluginInstance } from 'webpack';
import { isChildPath } from '@backstage/cli-common';
import { LernaPackage } from './types';
// Enables proper resolution of packages when linking in external packages.
// Without this the packages would depend on dependencies in the node_modules
// of the external packages themselves, leading to module duplication
export class LinkedPackageResolvePlugin implements ResolvePlugin {
export class LinkedPackageResolvePlugin implements WebpackPluginInstance {
constructor(
private readonly targetModules: string,
private readonly packages: LernaPackage[],
+7 -13
View File
@@ -26,23 +26,17 @@ export async function serveBackend(options: BackendServeOptions) {
isDev: true,
});
const compiler = webpack(config);
const watcher = compiler.watch(
{
poll: true,
},
(err: Error) => {
if (err) {
console.error(err);
} else console.log('Build succeeded');
},
);
const compiler = webpack(config, (err: Error | undefined) => {
if (err) {
console.log('here');
console.error(err);
} else console.log('Build succeeded');
});
const waitForExit = async () => {
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, () => {
watcher.close(() => console.log('Stopped watcher'));
compiler.close(() => console.log('Stopped watcher'));
// exit instead of resolve. The process is shutting down and resolving a promise here logs an error
process.exit();
});
+26 -15
View File
@@ -69,6 +69,10 @@ export async function buildBundle(options: BuildOptions) {
throw new Error(`Failed to compile.\n${error.message || error}`);
});
if (!stats) {
throw new Error('No stats returned');
}
if (statsJsonEnabled) {
// No @types/bfj
await require('bfj').write(
@@ -87,25 +91,32 @@ export async function buildBundle(options: BuildOptions) {
}
async function build(compiler: webpack.Compiler, isCi: boolean) {
const stats = await new Promise<webpack.Stats>((resolve, reject) => {
compiler.run((err, buildStats) => {
if (err) {
if (err.message) {
const { errors } = formatWebpackMessages({
errors: [err.message],
warnings: new Array<string>(),
} as webpack.Stats.ToJsonOutput);
const stats = await new Promise<webpack.Stats | undefined>(
(resolve, reject) => {
compiler.run((err, buildStats) => {
if (err) {
if (err.message) {
const { errors } = formatWebpackMessages({
errors: [err.message],
warnings: new Array<string>(),
_showErrors: true,
_showWarnings: true,
});
throw new Error(errors[0]);
throw new Error(errors[0]);
} else {
reject(err);
}
} else {
reject(err);
resolve(buildStats);
}
} else {
resolve(buildStats);
}
});
});
});
},
);
if (!stats) {
throw new Error('No stats provided');
}
const { errors, warnings } = formatWebpackMessages(
stats.toJson({ all: false, warnings: true, errors: true }),
);
+42 -30
View File
@@ -19,8 +19,8 @@ import { resolve as resolvePath } from 'path';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
import StartServerPlugin from 'start-server-webpack-plugin';
import webpack from 'webpack';
import { RunScriptWebpackPlugin } from 'run-script-webpack-plugin';
import webpack, { ProvidePlugin } from 'webpack';
import nodeExternals from 'webpack-node-externals';
import { isChildPath } from '@backstage/cli-common';
import { optimization } from './optimization';
@@ -98,10 +98,9 @@ export async function createConfig(
if (checksEnabled) {
plugins.push(
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: paths.targetTsConfig,
},
eslint: {
typescript: paths.targetTsConfig,
eslint: true,
eslintOptions: {
files: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'],
options: {
parserOptions: {
@@ -114,6 +113,15 @@ export async function createConfig(
);
}
// TODO(blam): process is no longer auto polyfilled by webpack in v5.
// we use the provide plugin to provide this polyfill, but lets look
// to remove this eventually!
plugins.push(
new ProvidePlugin({
process: 'process/browser',
}),
);
plugins.push(
new webpack.EnvironmentPlugin({
APP_CONFIG: options.frontendAppConfigs,
@@ -156,27 +164,33 @@ export async function createConfig(
return {
mode: isDev ? 'development' : 'production',
profile: false,
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
http2: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
optimization: optimization(options),
bail: false,
performance: {
hints: false, // we check the gzip size instead
},
devtool: isDev ? 'cheap-module-eval-source-map' : 'source-map',
devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map',
context: paths.targetPath,
entry: [require.resolve('react-hot-loader/patch'), paths.targetEntry],
resolve: {
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
mainFields: ['browser', 'module', 'main'],
fallback: {
module: false,
dgram: false,
dns: false,
fs: false,
http2: false,
net: false,
tls: false,
child_process: false,
/* new ignores */
path: false,
https: false,
http: false,
util: require.resolve('util/'),
},
plugins: [
new LinkedPackageResolvePlugin(paths.rootNodeModules, externalPkgs),
new ModuleScopePlugin(
@@ -200,7 +214,7 @@ export async function createConfig(
: 'static/[name].[chunkhash:8].chunk.js',
...(isDev
? {
devtoolModuleFilenameTemplate: info =>
devtoolModuleFilenameTemplate: (info: any) =>
`file:///${resolvePath(info.absoluteResourcePath).replace(
/\\/g,
'/',
@@ -237,7 +251,7 @@ export async function createBackendConfig(
? {
watch: true,
watchOptions: {
ignored: [/node_modules\/(?!\@backstage)/],
ignored: /node_modules\/(?!\@backstage)/,
},
}
: {}),
@@ -259,7 +273,7 @@ export async function createBackendConfig(
performance: {
hints: false, // we check the gzip size instead
},
devtool: isDev ? 'cheap-module-eval-source-map' : 'source-map',
devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map',
context: paths.targetPath,
entry: [
'webpack/hot/poll?100',
@@ -291,7 +305,7 @@ export async function createBackendConfig(
: '[name].[chunkhash:8].chunk.js',
...(isDev
? {
devtoolModuleFilenameTemplate: info =>
devtoolModuleFilenameTemplate: (info: any) =>
`file:///${resolvePath(info.absoluteResourcePath).replace(
/\\/g,
'/',
@@ -300,7 +314,7 @@ export async function createBackendConfig(
: {}),
},
plugins: [
new StartServerPlugin({
new RunScriptWebpackPlugin({
name: 'main.js',
nodeArgs: options.inspectEnabled ? ['--inspect'] : undefined,
}),
@@ -308,10 +322,9 @@ export async function createBackendConfig(
...(checksEnabled
? [
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: paths.targetTsConfig,
},
eslint: {
typescript: paths.targetTsConfig,
eslint: true,
eslintOptions: {
files: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'],
options: {
parserOptions: {
@@ -347,11 +360,10 @@ function nodeExternalsWithResolve(
});
return (
context: string,
request: string,
callback: webpack.ExternalsFunctionCallback,
{ context, request }: { context?: string; request?: string },
callback: any,
) => {
currentContext = context;
currentContext = context!;
return externals(context, request, callback);
};
}
+3 -3
View File
@@ -14,14 +14,14 @@
* limitations under the License.
*/
import { Options } from 'webpack';
import { WebpackOptionsNormalized, WebpackPluginInstance } from 'webpack';
import TerserPlugin from 'terser-webpack-plugin';
import { BundlingOptions } from './types';
import { isParallelDefault } from '../parallel';
export const optimization = (
options: BundlingOptions,
): Options.Optimization => {
): WebpackOptionsNormalized['optimization'] => {
const { isDev } = options;
return {
@@ -32,7 +32,7 @@ export const optimization = (
minimizer: [
new TerserPlugin({
parallel: options.parallel,
}),
}) as unknown as WebpackPluginInstance,
],
}
: {}),
+25 -19
View File
@@ -42,25 +42,31 @@ export async function serveBundle(options: ServeOptions) {
});
const compiler = webpack(config);
const server = new WebpackDevServer(compiler, {
hot: !process.env.CI,
contentBase: paths.targetPublic,
contentBasePublicPath: config.output?.publicPath,
publicPath: config.output?.publicPath,
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebookincubator/create-react-app/issues/387.
disableDotRule: true,
},
clientLogLevel: 'warning',
stats: 'errors-warnings',
https: url.protocol === 'https:',
host,
port,
proxy: pkg.proxy,
// When the dev server is behind a proxy, the host and public hostname differ
allowedHosts: [url.hostname],
});
const server = new WebpackDevServer(
compiler as any,
{
hot: !process.env.CI,
devMiddleware: {
publicPath: config.output?.publicPath as string,
stats: 'errors-warnings',
},
static: {
publicPath: config.output?.publicPath as string,
directory: paths.targetPublic,
},
historyApiFallback: {
// Paths with dots should still use the history fallback.
// See https://github.com/facebookincubator/create-react-app/issues/387.
disableDotRule: true,
},
https: url.protocol === 'https:',
host,
port,
proxy: pkg.proxy,
// When the dev server is behind a proxy, the host and public hostname differ
allowedHosts: [url.hostname],
} as any,
);
await new Promise<void>((resolve, reject) => {
server.listen(port, host, (err?: Error) => {
+10 -4
View File
@@ -14,13 +14,13 @@
* limitations under the License.
*/
import webpack, { Module, Plugin } from 'webpack';
import webpack, { ModuleOptions, WebpackPluginInstance } from 'webpack';
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import { svgrTemplate } from '../svgrTemplate';
type Transforms = {
loaders: Module['rules'];
plugins: Plugin[];
loaders: ModuleOptions['rules'];
plugins: WebpackPluginInstance[];
};
type TransformOptions = {
@@ -51,6 +51,12 @@ export const transforms = (options: TransformOptions): Transforms => {
production: !isDev,
},
},
{
test: /\.m?js/,
resolve: {
fullySpecified: false,
},
},
{
test: [/\.icon\.svg$/],
use: [
@@ -105,7 +111,7 @@ export const transforms = (options: TransformOptions): Transforms => {
},
];
const plugins = new Array<Plugin>();
const plugins = new Array<WebpackPluginInstance>();
if (isDev) {
plugins.push(new webpack.HotModuleReplacementPlugin());
+3 -3
View File
@@ -137,9 +137,9 @@ export class Lockfile {
}
// Find all versions currently in use
const versions = Array.from(
new Set(entries.map(e => e.version)),
).sort((v1, v2) => semver.rcompare(v1, v2));
const versions = Array.from(new Set(entries.map(e => e.version))).sort(
(v1, v2) => semver.rcompare(v1, v2),
);
// If we're not using at least 2 different versions we're done
if (versions.length < 2) {
+226 -1
View File
@@ -29,5 +29,230 @@ declare module '@svgr/rollup' {
}
declare module '@rollup/plugin-yaml';
declare module 'react-dev-utils/formatWebpackMessages' {
export default function (stats: any): {
errors: string[];
warnings: string[];
};
}
declare module 'terser-webpack-plugin';
declare module 'react-dev-utils/openBrowser' {
export default function (url: string): boolean;
}
declare module 'react-dev-utils/ModuleScopePlugin' {
import webpack = require('webpack');
export default class ModuleScopePlugin
implements webpack.WebpackPluginInstance
{
constructor(
appSrc: string | ReadonlyArray<string>,
allowedFiles?: ReadonlyArray<string>,
);
apply: (resolver: any) => void;
}
}
declare module 'react-dev-utils/FileSizeReporter' {
import webpack = require('webpack');
export interface OpaqueFileSizes {
root: string;
sizes: Record<string, number>;
}
/**
* Captures JS and CSS asset sizes inside the passed `buildFolder`. Save the
* result value to compare it after the build.
*/
export function measureFileSizesBeforeBuild(
buildFolder: string,
): Promise<OpaqueFileSizes>;
/**
* Prints the JS and CSS asset sizes after the build, and includes a size
* comparison with `previousFileSizes` that were captured earlier using
* `measureFileSizesBeforeBuild()`. `maxBundleGzipSize` and
* `maxChunkGzipSizemay` may optionally be specified to display a warning when
* the main bundle or a chunk exceeds the specified size (in bytes).
*/
export function printFileSizesAfterBuild(
webpackStats: webpack.Stats,
previousFileSizes: OpaqueFileSizes,
buildFolder: string,
maxBundleGzipSize?: number,
maxChunkGzipSize?: number,
): void;
}
declare module 'mini-css-extract-plugin' {
import webpack = require('webpack');
/**
* Lightweight CSS extraction webpack plugin.
*
* This plugin extracts CSS into separate files. It creates a CSS file per JS file which
* contains CSS. It supports On-Demand-Loading of CSS and SourceMaps.
*
* Configuration Detail: https://github.com/webpack-contrib/mini-css-extract-plugin#configuration
*/
export default class MiniCssExtractPlugin {
/**
* Webpack loader always used at the end of loaders list (ie. array index zero).
*/
static loader: string;
constructor(options?: MiniCssExtractPlugin.PluginOptions);
/**
* Apply the plugin
*/
apply(compiler: webpack.Compiler): void;
}
namespace MiniCssExtractPlugin {
interface PluginOptions {
/**
* Works like [`output.filename`](https://webpack.js.org/configuration/output/#outputfilename).
*/
filename?: Required<webpack.Configuration>['output']['filename'];
/**
* Works like [`output.chunkFilename`](https://webpack.js.org/configuration/output/#outputchunkfilename).
*/
chunkFilename?: string;
/**
* For projects where CSS ordering has been mitigated through consistent
* use of scoping or naming conventions, the CSS order warnings can be
* disabled by setting this flag to true for the plugin.
*/
ignoreOrder?: boolean;
/**
* Specify where to insert the link tag.
*
* A string value specifies a DOM query for a parent element to attach to.
*
* A function allows to override default behavior for non-entry CSS chunks.
* This code will run in the browser alongside your application. It is recommend
* to only use ECMA 5 features and syntax. The function won't have access to the
* scope of the webpack configuration module.
*
* @default function() { document.head.appendChild(linkTag); }
*/
insert?: string | ((linkTag: any) => void);
/**
* Specify additional html attributes to add to the link tag.
*
* Note: These are only applied to dynamically loaded css chunks. To modify link
* attributes for entry CSS chunks, please use html-webpack-plugin.
*/
attributes?: Record<string, string>;
/**
* This option allows loading asynchronous chunks with a custom link type, such as
* `<link type="text/css" ...>`.
*
* `false` disables the link `type` attribute.
*
* @default 'text/css'
*/
linkType?: string | false | 'text/css';
}
interface LoaderOptions {
/**
* Overrides [`output.publicPath`](https://webpack.js.org/configuration/output/#outputpublicpath).
* @default output.publicPath
*/
publicPath?: string | ((resourcePath: string, context: string) => string);
/**
* If false, the plugin will extract the CSS but **will not** emit the file
* @default true
*/
emit?: boolean;
/**
* By default, `mini-css-extract-plugin` generates JS modules that use the ES modules syntax.
* There are some cases in which using ES modules is beneficial,
* like in the case of module concatenation and tree shaking.
* @default true
*/
esModule?: boolean;
modules?: {
/**
* Enables/disables ES modules named export for locals.
*
* Names of locals are converted to camelCase. It is not allowed to use
* JavaScript reserved words in CSS class names. Options `esModule` and
* `modules.namedExport` in css-loader and MiniCssExtractPlugin.loader
* must be enabled.
*
* @default false
*/
namedExport?: boolean;
};
}
}
}
declare module 'fork-ts-checker-webpack-plugin/lib/ForkTsCheckerWebpackPlugin' {}
declare module 'webpack-node-externals' {
export default function webpackNodeExternals(
options?: webpackNodeExternals.Options,
): any;
namespace webpackNodeExternals {
type AllowlistOption = string | RegExp | AllowlistFunctionType;
type ImportTypeCallback = (moduleName: string) => string;
/** a function that accepts the module name and returns whether it should be included */
type AllowlistFunctionType = (moduleName: string) => boolean;
interface ModulesFromFileType {
exclude?: string | string[];
include?: string | string[];
}
interface Options {
/**
* An array for the externals to allow, so they will be included in the bundle.
* Can accept exact strings ('module_name'), regex patterns (/^module_name/), or a
* function that accepts the module name and returns whether it should be included.
* Important - if you have set aliases in your webpack config with the exact
* same names as modules in node_modules, you need to allowlist them so Webpack will know
* they should be bundled.
* @default []
*/
allowlist?: AllowlistOption[] | AllowlistOption;
/**
* @default ['.bin']
*/
binaryDirs?: string[];
/**
* The method in which unbundled modules will be required in the code. Best to leave as
* 'commonjs' for node modules.
* @default 'commonjs'
*/
importType?:
| 'var'
| 'this'
| 'commonjs'
| 'amd'
| 'umd'
| ImportTypeCallback;
/**
* The folder in which to search for the node modules.
* @default 'node_modules'
*/
modulesDir?: string;
/**
* Additional folders to look for node modules.
*/
additionalModuleDirs?: string[];
/**
* Read the modules from the package.json file instead of the node_modules folder.
* @default false
*/
modulesFromFile?: boolean | ModulesFromFileType;
/**
* @default false
*/
includeAbsolutePaths?: boolean;
}
}
}
@@ -24,9 +24,11 @@ const substituteMe = '${MY_SUBSTITUTION}';
const mySubstitution = 'fooSubstitution';
const env = jest.fn(async (name: string) => {
return ({
SECRET: 'my-secret',
} as { [name: string]: string })[name];
return (
{
SECRET: 'my-secret',
} as { [name: string]: string }
)[name];
});
const substitute: TransformFunc = async value => {
@@ -44,14 +46,16 @@ const substitute: TransformFunc = async value => {
};
const readFile = jest.fn(async (path: string) => {
const content = ({
[resolvePath(root, 'my-secret')]: 'secret',
[resolvePath(root, 'my-data.json')]: '{"a":{"b":{"c":42}}}',
[resolvePath(root, 'my-data.yaml')]: 'some:\n yaml:\n key: 7',
[resolvePath(root, 'my-data.yml')]: 'different: { key: hello }',
[resolvePath(root, 'invalid.yaml')]: 'foo: [}',
[resolvePath(root, `${mySubstitution}/my-data.json`)]: '{"foo":"bar"}',
} as { [key: string]: string })[path];
const content = (
{
[resolvePath(root, 'my-secret')]: 'secret',
[resolvePath(root, 'my-data.json')]: '{"a":{"b":{"c":42}}}',
[resolvePath(root, 'my-data.yaml')]: 'some:\n yaml:\n key: 7',
[resolvePath(root, 'my-data.yml')]: 'different: { key: hello }',
[resolvePath(root, 'invalid.yaml')]: 'foo: [}',
[resolvePath(root, `${mySubstitution}/my-data.json`)]: '{"foo":"bar"}',
} as { [key: string]: string }
)[path];
if (!content) {
throw new Error('File not found!');
@@ -17,10 +17,12 @@
import { createSubstitutionTransform } from './substitution';
const env = jest.fn(async (name: string) => {
return ({
SECRET: 'my-secret',
TOKEN: 'my-token',
} as { [name: string]: string })[name];
return (
{
SECRET: 'my-secret',
TOKEN: 'my-token',
} as { [name: string]: string }
)[name];
});
const substituteTransform = createSubstitutionTransform(env);
+6 -6
View File
@@ -64,9 +64,9 @@ function expectValidValues(config: ConfigReader) {
strings: ['string1', 'string2'],
});
expect(config.getConfig('nested').getString('string')).toBe('string');
expect(
config.getOptionalConfig('nested')!.getStringArray('strings'),
).toEqual(['string1', 'string2']);
expect(config.getOptionalConfig('nested')!.getStringArray('strings')).toEqual(
['string1', 'string2'],
);
expect(config.getOptional('missing')).toBe(undefined);
expect(config.getOptionalConfig('missing')).toBe(undefined);
expect(config.getOptionalConfigArray('missing')).toBe(undefined);
@@ -234,9 +234,9 @@ describe('ConfigReader', () => {
expect(withLogCollector(() => config.getOptional('a'))).toMatchObject({
warn: [],
});
expect(
withLogCollector(() => config.getOptionalString('a')),
).toMatchObject({ warn: [] });
expect(withLogCollector(() => config.getOptionalString('a'))).toMatchObject(
{ warn: [] },
);
expect(
withLogCollector(() => config.getOptionalConfigArray('b')),
).toMatchObject({ warn: [] });
+8 -10
View File
@@ -68,9 +68,7 @@ export class AlertApiForwarder implements AlertApi {
//
// @public (undocumented)
export type ApiFactoryHolder = {
get<T>(
api: ApiRef<T>,
):
get<T>(api: ApiRef<T>):
| ApiFactory<
T,
T,
@@ -86,9 +84,7 @@ export type ApiFactoryHolder = {
// @public
export class ApiFactoryRegistry implements ApiFactoryHolder {
// (undocumented)
get<T>(
api: ApiRef<T>,
):
get<T>(api: ApiRef<T>):
| ApiFactory<
T,
T,
@@ -105,7 +101,7 @@ export class ApiFactoryRegistry implements ApiFactoryHolder {
Impl extends Api,
Deps extends {
[name in string]: unknown;
}
},
>(scope: ApiFactoryScope, factory: ApiFactory<Api, Impl, Deps>): boolean;
}
@@ -211,7 +207,7 @@ export type AppOptions = {
export type AppRouteBinder = <
ExternalRoutes extends {
[name: string]: ExternalRouteRef;
}
},
>(
externalRoutes: ExternalRoutes,
targetRoutes: PartialKeys<
@@ -469,7 +465,8 @@ export class OAuth2
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionApi {
SessionApi
{
// Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts
constructor(options: Options);
// Warning: (ae-forgotten-export) The symbol "CreateOptions" needs to be exported by the entry point index.d.ts
@@ -561,7 +558,8 @@ export class OneLoginAuth {
//
// @public (undocumented)
export class SamlAuth
implements ProfileInfoApi, BackstageIdentityApi, SessionApi {
implements ProfileInfoApi, BackstageIdentityApi, SessionApi
{
// Warning: (ae-forgotten-export) The symbol "SamlSession" needs to be exported by the entry point index.d.ts
constructor(sessionManager: SessionManager<SamlSession>);
// Warning: (ae-forgotten-export) The symbol "AuthApiCreateOptions" needs to be exported by the entry point index.d.ts
@@ -194,8 +194,7 @@ describe('FeatureFlags', () => {
it('throws an error if length is greater than 150 characters', () => {
expect(() =>
featureFlags.registerFlag({
name:
'loremipsumdolorsitametconsecteturadipiscingelitnuncvitaeportaexaullamcorperturpismaurisutmattisnequemorbisediaculisauguevivamuspulvinarcursuseratblandithendreritquisqueuttinciduntmagnavestibulumblanditaugueat',
name: 'loremipsumdolorsitametconsecteturadipiscingelitnuncvitaeportaexaullamcorperturpismaurisutmattisnequemorbisediaculisauguevivamuspulvinarcursuseratblandithendreritquisqueuttinciduntmagnavestibulumblanditaugueat',
pluginId: 'plugin-three',
}),
).toThrow(/not exceed 150 characters/i);
@@ -65,7 +65,8 @@ class OAuth2
OpenIdConnectApi,
ProfileInfoApi,
BackstageIdentityApi,
SessionApi {
SessionApi
{
static create({
discoveryApi,
environment = 'development',
@@ -185,9 +185,10 @@ describe('ApiProvider', () => {
});
describe('v1 consumer', () => {
const ApiContext = getGlobalSingleton<
Context<VersionedValue<{ 1: ApiHolder }>>
>('api-context');
const ApiContext =
getGlobalSingleton<Context<VersionedValue<{ 1: ApiHolder }>>>(
'api-context',
);
function useMockApiV1<T>(apiRef: ApiRef<T>): T {
const impl = useContext(ApiContext)?.atVersion(1)?.get(apiRef);
+44 -42
View File
@@ -107,6 +107,20 @@ export function generateBoundRoutes(bindRoutes: AppOptions['bindRoutes']) {
return result;
}
/**
* Get the app base path from the configured app baseUrl.
*
* The returned path does not have a trailing slash.
*/
function getBasePath(configApi: Config) {
let { pathname } = new URL(
configApi.getOptionalString('app.baseUrl') ?? '/',
'http://dummy.dev', // baseUrl can be specified as just a path
);
pathname = pathname.replace(/\/*$/, '');
return pathname;
}
type FullAppOptions = {
apis: Iterable<AnyApiFactory>;
icons: NonNullable<AppOptions['icons']>;
@@ -216,37 +230,33 @@ export class PrivateAppImpl implements BackstageApp {
[],
);
const {
routePaths,
routeParents,
routeObjects,
featureFlags,
} = useMemo(() => {
const result = traverseElementTree({
root: children,
discoverers: [childDiscoverer, routeElementDiscoverer],
collectors: {
routePaths: routePathCollector,
routeParents: routeParentCollector,
routeObjects: routeObjectCollector,
collectedPlugins: pluginCollector,
featureFlags: featureFlagCollector,
},
});
const { routePaths, routeParents, routeObjects, featureFlags } =
useMemo(() => {
const result = traverseElementTree({
root: children,
discoverers: [childDiscoverer, routeElementDiscoverer],
collectors: {
routePaths: routePathCollector,
routeParents: routeParentCollector,
routeObjects: routeObjectCollector,
collectedPlugins: pluginCollector,
featureFlags: featureFlagCollector,
},
});
validateRoutes(result.routePaths, result.routeParents);
validateRoutes(result.routePaths, result.routeParents);
// TODO(Rugvip): Restructure the public API so that we can get an immediate view of
// the app, rather than having to wait for the provider to render.
// For now we need to push the additional plugins we find during
// collection and then make sure we initialize things afterwards.
result.collectedPlugins.forEach(plugin => this.plugins.add(plugin));
this.verifyPlugins(this.plugins);
// TODO(Rugvip): Restructure the public API so that we can get an immediate view of
// the app, rather than having to wait for the provider to render.
// For now we need to push the additional plugins we find during
// collection and then make sure we initialize things afterwards.
result.collectedPlugins.forEach(plugin => this.plugins.add(plugin));
this.verifyPlugins(this.plugins);
// Initialize APIs once all plugins are available
this.getApiHolder();
return result;
}, [children]);
// Initialize APIs once all plugins are available
this.getApiHolder();
return result;
}, [children]);
const loadedConfig = useConfigLoader(
this.configLoader,
@@ -302,6 +312,7 @@ export class PrivateAppImpl implements BackstageApp {
routeParents={routeParents}
routeObjects={routeObjects}
routeBindings={generateBoundRoutes(this.bindRoutes)}
basePath={getBasePath(loadedConfig.api)}
>
{children}
</RoutingProvider>
@@ -314,10 +325,8 @@ export class PrivateAppImpl implements BackstageApp {
}
getRouter(): ComponentType<{}> {
const {
Router: RouterComponent,
SignInPage: SignInPageComponent,
} = this.components;
const { Router: RouterComponent, SignInPage: SignInPageComponent } =
this.components;
// This wraps the sign-in page and waits for sign-in to be completed before rendering the app
const SignInPageWrapper = ({
@@ -339,14 +348,7 @@ export class PrivateAppImpl implements BackstageApp {
const AppRouter = ({ children }: PropsWithChildren<{}>) => {
const configApi = useApi(configApiRef);
let { pathname } = new URL(
configApi.getOptionalString('app.baseUrl') ?? '/',
'http://dummy.dev', // baseUrl can be specified as just a path
);
if (pathname.endsWith('/')) {
pathname = pathname.replace(/\/$/, '');
}
const mountPath = `${getBasePath(configApi)}/*`;
// If the app hasn't configured a sign-in page, we just continue as guest.
if (!SignInPageComponent) {
@@ -361,7 +363,7 @@ export class PrivateAppImpl implements BackstageApp {
return (
<RouterComponent>
<Routes>
<Route path={`${pathname}/*`} element={<>{children}</>} />
<Route path={mountPath} element={<>{children}</>} />
</Routes>
</RouterComponent>
);
@@ -371,7 +373,7 @@ export class PrivateAppImpl implements BackstageApp {
<RouterComponent>
<SignInPageWrapper component={SignInPageComponent}>
<Routes>
<Route path={`${pathname}/*`} element={<>{children}</>} />
<Route path={mountPath} element={<>{children}</>} />
</Routes>
</SignInPageWrapper>
</RouterComponent>
@@ -22,9 +22,10 @@ import { AppContext as AppContextV1 } from './types';
import { AppContextProvider } from './AppContext';
describe('v1 consumer', () => {
const AppContext = getGlobalSingleton<
Context<VersionedValue<{ 1: AppContextV1 }>>
>('app-context');
const AppContext =
getGlobalSingleton<Context<VersionedValue<{ 1: AppContextV1 }>>>(
'app-context',
);
function useMockAppV1(): AppContextV1 {
const impl = useContext(AppContext)?.atVersion(1);
+1 -1
View File
@@ -60,7 +60,7 @@ export const defaultConfigLoader: AppConfigLoader = async (
if (!Array.isArray(appConfig)) {
throw new Error('Static configuration has invalid format');
}
const configs = (appConfig.slice() as unknown) as AppConfig[];
const configs = appConfig.slice() as unknown as AppConfig[];
// Avoiding this string also being replaced at runtime
if (
+3 -3
View File
@@ -105,14 +105,14 @@ type KeysWithType<Obj extends { [key in string]: any }, Type> = {
*/
type PartialKeys<
Map extends { [name in string]: any },
Keys extends keyof Map
Keys extends keyof Map,
> = Partial<Pick<Map, Keys>> & Required<Omit<Map, Keys>>;
/**
* Creates a map of target routes with matching parameters based on a map of external routes.
*/
type TargetRouteMap<
ExternalRoutes extends { [name: string]: ExternalRouteRef }
ExternalRoutes extends { [name: string]: ExternalRouteRef },
> = {
[name in keyof ExternalRoutes]: ExternalRoutes[name] extends ExternalRouteRef<
infer Params,
@@ -123,7 +123,7 @@ type TargetRouteMap<
};
export type AppRouteBinder = <
ExternalRoutes extends { [name: string]: ExternalRouteRef }
ExternalRoutes extends { [name: string]: ExternalRouteRef },
>(
externalRoutes: ExternalRoutes,
targetRoutes: PartialKeys<
@@ -28,7 +28,7 @@ const ERROR_MESSAGE = 'Import this from @backstage/core-plugin-api';
/** @deprecated Import from @backstage/core-plugin-api instead */
export function createRoutableExtension<
T extends (props: any) => JSX.Element | null
T extends (props: any) => JSX.Element | null,
>(_options: {
component: () => Promise<T>;
mountPoint: RouteRef;
@@ -38,14 +38,14 @@ export function createRoutableExtension<
/** @deprecated Import from @backstage/core-plugin-api instead */
export function createComponentExtension<
T extends (props: any) => JSX.Element | null
T extends (props: any) => JSX.Element | null,
>(_options: { component: ComponentLoader<T> }): Extension<T> {
throw new Error(ERROR_MESSAGE);
}
/** @deprecated Import from @backstage/core-plugin-api instead */
export function createReactExtension<
T extends (props: any) => JSX.Element | null
T extends (props: any) => JSX.Element | null,
>(_options: {
component: ComponentLoader<T>;
data?: Record<string, unknown>;
@@ -126,8 +126,7 @@ describe('DefaultAuthConnector', () => {
expect(popupSpy).toBeCalledTimes(1);
expect(popupSpy.mock.calls[0][0]).toMatchObject({
url:
'http://my-host/api/auth/my-provider/start?scope=a%20b&env=production',
url: 'http://my-host/api/auth/my-provider/start?scope=a%20b&env=production',
});
await expect(sessionPromise).resolves.toEqual({
@@ -175,8 +174,7 @@ describe('DefaultAuthConnector', () => {
expect(popupSpy).toBeCalledTimes(1);
expect(popupSpy.mock.calls[0][0]).toMatchObject({
url:
'http://my-host/api/auth/my-provider/start?scope=-ab-&env=production',
url: 'http://my-host/api/auth/my-provider/start?scope=-ab-&env=production',
});
});
});
@@ -61,7 +61,8 @@ function defaultJoinScopes(scopes: Set<string>) {
* via the OAuthRequestApi.
*/
export class DefaultAuthConnector<AuthSession>
implements AuthConnector<AuthSession> {
implements AuthConnector<AuthSession>
{
private readonly discoveryApi: DiscoveryApi;
private readonly environment: string;
private readonly provider: AuthProvider & { id: string };
@@ -30,8 +30,7 @@ describe('showLoginPopup', () => {
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
const payloadPromise = showLoginPopup({
url:
'my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb',
url: 'my-origin/api/backend/auth/start?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fa%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fb',
name: 'test-popup',
origin: 'my-origin',
});
+4 -2
View File
@@ -29,7 +29,8 @@ import ObservableImpl from 'zen-observable';
* See http://reactivex.io/documentation/subject.html
*/
export class PublishSubject<T>
implements Observable<T>, ZenObservable.SubscriptionObserver<T> {
implements Observable<T>, ZenObservable.SubscriptionObserver<T>
{
private isClosed = false;
private terminatingError?: Error;
@@ -121,7 +122,8 @@ export class PublishSubject<T>
* See http://reactivex.io/documentation/subject.html
*/
export class BehaviorSubject<T>
implements Observable<T>, ZenObservable.SubscriptionObserver<T> {
implements Observable<T>, ZenObservable.SubscriptionObserver<T>
{
private isClosed = false;
private currentValue: T;
private terminatingError?: Error;
@@ -29,7 +29,7 @@ export type VersionedValue<Versions extends { [version: number]: any }> = {
* Creates a container for a map of versioned values that implements VersionedValue.
*/
export function createVersionedValueMap<
Versions extends { [version: number]: any }
Versions extends { [version: number]: any },
>(versions: Versions): VersionedValue<Versions> {
Object.freeze(versions);
return {
@@ -37,11 +37,11 @@ function makeRouteRenderer(node: ReactNode) {
<Wrapper>
<AppContextProvider
appContext={
({
{
getComponents: () => ({
NotFoundErrorPage: () => <>Not Found</>,
}),
} as unknown) as AppContext
} as unknown as AppContext
}
>
<MemoryRouter initialEntries={[path]} children={node} />
@@ -65,7 +65,7 @@ const externalRef4 = createExternalRouteRef({
describe('RouteResolver', () => {
it('should not resolve anything with an empty resolver', () => {
const r = new RouteResolver(new Map(), new Map(), [], new Map());
const r = new RouteResolver(new Map(), new Map(), [], new Map(), '');
expect(r.resolve(ref1, '/')?.()).toBe(undefined);
expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined);
@@ -85,6 +85,7 @@ describe('RouteResolver', () => {
new Map(),
[{ routeRefs: new Set([ref1]), path: '/my-route', ...rest }],
new Map(),
'',
);
expect(r.resolve(ref1, '/')?.()).toBe('/my-route');
@@ -99,6 +100,29 @@ describe('RouteResolver', () => {
expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined);
});
it('should resolve an absolute route and an app base path', () => {
const r = new RouteResolver(
new Map([[ref1, '/my-route']]),
new Map(),
[{ routeRefs: new Set([ref1]), path: '/my-route', ...rest }],
new Map(),
'/base',
);
expect(r.resolve(ref1, '/')?.()).toBe('/base/my-route');
expect(r.resolve(ref2, '/')?.({ x: '1x' })).toBe(undefined);
expect(r.resolve(subRef1, '/')?.()).toBe('/base/my-route/foo');
expect(r.resolve(subRef2, '/')?.({ a: '2a' })).toBe(
'/base/my-route/foo/2a',
);
expect(r.resolve(subRef3, '/')?.({ x: '3x' })).toBe(undefined);
expect(r.resolve(subRef4, '/')?.({ x: '4x', a: '4a' })).toBe(undefined);
expect(r.resolve(externalRef1, '/')?.()).toBe(undefined);
expect(r.resolve(externalRef2, '/')?.()).toBe(undefined);
expect(r.resolve(externalRef3, '/')?.({ x: '5x' })).toBe(undefined);
expect(r.resolve(externalRef4, '/')?.({ x: '6x' })).toBe(undefined);
});
it('should resolve an absolute route with a param and with a parent', () => {
const r = new RouteResolver(
new Map<RouteRef, string>([
@@ -122,6 +146,7 @@ describe('RouteResolver', () => {
[externalRef3, ref2],
[externalRef4, subRef3],
]),
'',
);
expect(r.resolve(ref1, '/')?.()).toBe('/my-route');
@@ -179,6 +204,7 @@ describe('RouteResolver', () => {
},
],
new Map<ExternalRouteRef, RouteRef | SubRouteRef>(),
'',
);
expect(r.resolve(ref2, '/')?.({ x: 'x' })).toBe('/root/x');
@@ -232,6 +258,7 @@ describe('RouteResolver', () => {
[externalRef3, ref2],
[externalRef4, subRef3],
]),
'',
);
const l = '/my-grandparent/my-y/my-parent/my-x';
@@ -187,6 +187,7 @@ export class RouteResolver {
ExternalRouteRef,
RouteRef | SubRouteRef
>,
private readonly appBasePath: string, // base path without a trailing slash
) {}
resolve<Params extends AnyParams>(
@@ -209,13 +210,15 @@ export class RouteResolver {
// Next we figure out the base path, which is the combination of the common parent path
// between our current location and our target location, as well as the additional path
// that is the difference between the parent path and the base of our target location.
const basePath = resolveBasePath(
targetRef,
sourceLocation,
this.routePaths,
this.routeParents,
this.routeObjects,
);
const basePath =
this.appBasePath +
resolveBasePath(
targetRef,
sourceLocation,
this.routePaths,
this.routeParents,
this.routeObjects,
);
const routeFunc: RouteFunc<Params> = (...[params]) => {
return basePath + generatePath(targetPath, params);
@@ -152,6 +152,7 @@ function withRoutingProvider(
routeParents={routeParents}
routeObjects={routeObjects}
routeBindings={new Map(routeBindings)}
basePath=""
>
{root}
</RoutingProvider>
@@ -330,9 +331,10 @@ describe('discovery', () => {
});
describe('v1 consumer', () => {
const RoutingContext = getGlobalSingleton<
Context<VersionedValue<{ 1: RouteResolver }>>
>('routing-context');
const RoutingContext =
getGlobalSingleton<Context<VersionedValue<{ 1: RouteResolver }>>>(
'routing-context',
);
function useMockRouteRefV1(
routeRef: AnyRouteRef,
@@ -367,6 +369,7 @@ describe('v1 consumer', () => {
routeParents={new Map()}
routeObjects={[]}
routeBindings={new Map()}
basePath="/base"
children={children}
/>
),
@@ -375,8 +378,8 @@ describe('v1 consumer', () => {
expect(renderedHook.result.current).toBe(undefined);
renderedHook.rerender({ routeRef: routeRef2 });
expect(renderedHook.result.current?.()).toBe('/foo');
expect(renderedHook.result.current?.()).toBe('/base/foo');
renderedHook.rerender({ routeRef: routeRef3 });
expect(renderedHook.result.current?.({ x: 'my-x' })).toBe('/bar/my-x');
expect(renderedHook.result.current?.({ x: 'my-x' })).toBe('/base/bar/my-x');
});
});
@@ -38,6 +38,7 @@ type ProviderProps = {
routeParents: Map<RouteRef, RouteRef | undefined>;
routeObjects: BackstageRouteObject[];
routeBindings: Map<ExternalRouteRef, RouteRef | SubRouteRef>;
basePath?: string;
children: ReactNode;
};
@@ -46,6 +47,7 @@ export const RoutingProvider = ({
routeParents,
routeObjects,
routeBindings,
basePath = '',
children,
}: ProviderProps) => {
const resolver = new RouteResolver(
@@ -53,6 +55,7 @@ export const RoutingProvider = ({
routeParents,
routeObjects,
routeBindings,
basePath,
);
const versionedValue = createVersionedValueMap({ 1: resolver });
+1 -1
View File
@@ -75,7 +75,7 @@ export function isSubRouteRef<Params extends AnyParams>(
export function isExternalRouteRef<
Params extends AnyParams,
Optional extends boolean
Optional extends boolean,
>(
routeRef:
| RouteRef<Params>
+2 -1
View File
@@ -33,6 +33,7 @@
"@backstage/core-plugin-api": "^0.1.5",
"@backstage/errors": "^0.1.1",
"@backstage/theme": "^0.2.9",
"@material-table/core": "^3.1.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -50,7 +51,6 @@
"dagre": "^0.8.5",
"immer": "^9.0.1",
"lodash": "^4.17.15",
"@material-table/core": "^3.1.0",
"pluralize": "^8.0.0",
"prop-types": "^15.7.2",
"qs": "^6.9.4",
@@ -84,6 +84,7 @@
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"@types/react-helmet": "^6.1.0",
"@types/react-syntax-highlighter": "^13.5.2",
"@types/zen-observable": "^0.8.0"
},
"files": [
@@ -38,10 +38,8 @@ export function DefaultNode({ node: { id } }: RenderNodeProps) {
React.useLayoutEffect(() => {
// set the width to the length of the ID
if (idRef.current) {
let {
height: renderedHeight,
width: renderedWidth,
} = idRef.current.getBBox();
let { height: renderedHeight, width: renderedWidth } =
idRef.current.getBBox();
renderedHeight = Math.round(renderedHeight);
renderedWidth = Math.round(renderedWidth);
@@ -132,10 +132,8 @@ export function DependencyGraph({
container.call(zoom);
const {
width: newContainerWidth,
height: newContainerHeight,
} = node.getBoundingClientRect();
const { width: newContainerWidth, height: newContainerHeight } =
node.getBoundingClientRect();
if (containerWidth !== newContainerWidth) {
setContainerWidth(newContainerWidth);
}
@@ -71,10 +71,8 @@ export function Edge({
React.useLayoutEffect(() => {
// set the label width to the actual rendered width to properly layout graph
if (labelRef.current) {
let {
height: renderedHeight,
width: renderedWidth,
} = labelRef.current.getBBox();
let { height: renderedHeight, width: renderedWidth } =
labelRef.current.getBBox();
renderedHeight = Math.round(renderedHeight);
renderedWidth = Math.round(renderedWidth);
@@ -46,10 +46,8 @@ export function Node({
React.useLayoutEffect(() => {
// set the node width to the actual rendered width to properly layout graph
if (nodeRef.current) {
let {
height: renderedHeight,
width: renderedWidth,
} = nodeRef.current.getBBox();
let { height: renderedHeight, width: renderedWidth } =
nodeRef.current.getBBox();
renderedHeight = Math.round(renderedHeight);
renderedWidth = Math.round(renderedWidth);
@@ -38,9 +38,10 @@ function useCalloutStates(): {
return { states, setState };
}
function useCalloutHasBeenSeen(
featureId: string,
): { seen: boolean | undefined; markSeen: () => void } {
function useCalloutHasBeenSeen(featureId: string): {
seen: boolean | undefined;
markSeen: () => void;
} {
const { states, setState } = useCalloutStates();
const markSeen = useCallback(() => {
@@ -50,9 +51,10 @@ function useCalloutHasBeenSeen(
return { seen: states[featureId] === true, markSeen };
}
export function useShowCallout(
featureId: string,
): { show: boolean; hide: () => void } {
export function useShowCallout(featureId: string): {
show: boolean;
hide: () => void;
} {
const { seen, markSeen } = useCalloutHasBeenSeen(featureId);
return { show: seen === false, hide: markSeen };
}
@@ -96,14 +96,14 @@ interface StyleProps extends WithStyles {
children?: React.ReactNode;
}
export const MetadataList = withStyles(
listStyles,
)(({ classes, children }: StyleProps) => (
<ul className={classes.root}>{children}</ul>
));
export const MetadataList = withStyles(listStyles)(
({ classes, children }: StyleProps) => (
<ul className={classes.root}>{children}</ul>
),
);
export const MetadataListItem = withStyles(
listItemStyles,
)(({ classes, children }: StyleProps) => (
<li className={classes.root}>{children}</li>
));
export const MetadataListItem = withStyles(listItemStyles)(
({ classes, children }: StyleProps) => (
<li className={classes.root}>{children}</li>
),
);
@@ -44,16 +44,16 @@ interface StyleProps extends WithStyles {
children?: React.ReactNode;
}
// Sub Components
const StyledList = withStyles(
listStyle,
)(({ classes, children }: StyleProps) => (
<MetadataList classes={classes}>{children}</MetadataList>
));
const StyledNestedList = withStyles(
nestedListStyle,
)(({ classes, children }: StyleProps) => (
<MetadataList classes={classes}>{children}</MetadataList>
));
const StyledList = withStyles(listStyle)(
({ classes, children }: StyleProps) => (
<MetadataList classes={classes}>{children}</MetadataList>
),
);
const StyledNestedList = withStyles(nestedListStyle)(
({ classes, children }: StyleProps) => (
<MetadataList classes={classes}>{children}</MetadataList>
),
);
function renderList(list: Array<any>, nested?: boolean) {
const values = list.map((item: any, index: number) => (
@@ -20,9 +20,11 @@ import { Content } from '../../layout/Content';
import { HeaderTabs } from '../../layout/HeaderTabs';
import { SubRoute } from './types';
export function useSelectedSubRoute(
subRoutes: SubRoute[],
): { index: number; route: SubRoute; element: JSX.Element } {
export function useSelectedSubRoute(subRoutes: SubRoute[]): {
index: number;
route: SubRoute;
element: JSX.Element;
} {
const params = useParams();
const routes = subRoutes.map(({ path, children }) => ({
@@ -285,9 +285,10 @@ export function Table<T extends object = {}>({
const [filtersOpen, setFiltersOpen] = useState(
calculatedInitialState.filtersOpen,
);
const toggleFilters = useCallback(() => setFiltersOpen(v => !v), [
setFiltersOpen,
]);
const toggleFilters = useCallback(
() => setFiltersOpen(v => !v),
[setFiltersOpen],
);
const [selectedFiltersLength, setSelectedFiltersLength] = useState(0);
const [tableData, setTableData] = useState(data as any[]);
const [selectedFilters, setSelectedFilters] = useState(
@@ -42,8 +42,7 @@ const DEFAULT_SUPPORT_CONFIG: SupportConfig = {
{
// TODO: Update to dedicated support page on backstage.io/docs
title: 'Add `app.support` config key',
url:
'https://github.com/andrewthauer/backstage/blob/master/app-config.yaml',
url: 'https://github.com/andrewthauer/backstage/blob/master/app-config.yaml',
},
],
},
@@ -42,18 +42,14 @@ describe('<ErrorBoundary/>', () => {
it('should render error boundary with and without error', async () => {
const { error } = await withLogCollector(['error'], async () => {
const apis = ApiRegistry.with(errorApiRef, new MockErrorApi());
const {
rerender,
queryByRole,
getByRole,
getByText,
} = await renderInTestApp(
<ApiProvider apis={apis}>
<ErrorBoundary>
<Bomb />
</ErrorBoundary>
</ApiProvider>,
);
const { rerender, queryByRole, getByRole, getByText } =
await renderInTestApp(
<ApiProvider apis={apis}>
<ErrorBoundary>
<Bomb />
</ErrorBoundary>
</ApiProvider>,
);
expect(queryByRole('alert')).not.toBeInTheDocument();
expect(getByText(/working component/i)).toBeInTheDocument();
@@ -133,10 +133,8 @@ export const SidebarIntro = () => {
starredItemsDismissed: false,
recentlyViewedItemsDismissed: false,
};
const [
dismissedIntro,
setDismissedIntro,
] = useLocalStorage<SidebarIntroLocalStorage>(SIDEBAR_INTRO_LOCAL_STORAGE);
const [dismissedIntro, setDismissedIntro] =
useLocalStorage<SidebarIntroLocalStorage>(SIDEBAR_INTRO_LOCAL_STORAGE);
const { starredItemsDismissed, recentlyViewedItemsDismissed } =
dismissedIntro ?? {};
@@ -34,7 +34,12 @@ import React, {
useContext,
useState,
} from 'react';
import { NavLink, NavLinkProps } from 'react-router-dom';
import {
Link,
NavLinkProps,
useLocation,
useResolvedPath,
} from 'react-router-dom';
import { sidebarConfig, SidebarContext } from './config';
const useStyles = makeStyles<BackstageTheme>(theme => {
@@ -147,6 +152,54 @@ function isButtonItem(
return (props as SidebarItemLinkProps).to === undefined;
}
// TODO(Rugvip): Remove this once NavLink is updated in react-router-dom.
// This is needed because react-router doesn't handle the path comparison
// properly yet, matching for example /foobar with /foo.
export const WorkaroundNavLink = React.forwardRef<
HTMLAnchorElement,
NavLinkProps
>(function WorkaroundNavLinkWithRef(
{
to,
end,
style,
className,
activeStyle,
caseSensitive,
activeClassName = 'active',
'aria-current': ariaCurrentProp = 'page',
...rest
},
ref,
) {
let { pathname: locationPathname } = useLocation();
let { pathname: toPathname } = useResolvedPath(to);
if (!caseSensitive) {
locationPathname = locationPathname.toLowerCase();
toPathname = toPathname.toLowerCase();
}
let isActive = locationPathname === toPathname;
if (!isActive && !end) {
// This is the behavior that is different from the original NavLink
isActive = locationPathname.startsWith(`${toPathname}/`);
}
const ariaCurrent = isActive ? ariaCurrentProp : undefined;
return (
<Link
{...rest}
to={to}
ref={ref}
aria-current={ariaCurrent}
style={{ ...style, ...(isActive ? activeStyle : undefined) }}
className={clsx([className, isActive ? activeClassName : undefined])}
/>
);
});
export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
const {
icon: Icon,
@@ -211,7 +264,7 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
}
return (
<NavLink
<WorkaroundNavLink
{...childProps}
activeClassName={classes.selected}
to={props.to}
@@ -220,7 +273,7 @@ export const SidebarItem = forwardRef<any, SidebarItemProps>((props, ref) => {
{...navLinkProps}
>
{content}
</NavLink>
</WorkaroundNavLink>
);
});
+15 -17
View File
@@ -58,7 +58,7 @@ export type ApiFactory<
Impl extends Api,
Deps extends {
[name in string]: unknown;
}
},
> = {
api: ApiRef<Api>;
deps: TypesToApiRefs<Deps>;
@@ -86,7 +86,7 @@ export type ApiRef<T> = {
export type ApiRefsToTypes<
T extends {
[key in string]: ApiRef<unknown>;
}
},
> = {
[key in keyof T]: ApiRefType<T[key]>;
};
@@ -216,7 +216,7 @@ export type BackstageIdentityApi = {
// @public (undocumented)
export type BackstagePlugin<
Routes extends AnyRoutes = {},
ExternalRoutes extends AnyExternalRoutes = {}
ExternalRoutes extends AnyExternalRoutes = {},
> = {
getId(): string;
output(): PluginOutput[];
@@ -253,7 +253,7 @@ export function createApiFactory<
Impl extends Api,
Deps extends {
[name in string]: unknown;
}
},
>(factory: ApiFactory<Api, Impl, Deps>): ApiFactory<Api, Impl, Deps>;
// @public (undocumented)
@@ -272,7 +272,7 @@ export function createApiRef<T>(config: ApiRefConfig): ApiRef<T>;
//
// @public (undocumented)
export function createComponentExtension<
T extends (props: any) => JSX.Element | null
T extends (props: any) => JSX.Element | null,
>(options: { component: ComponentLoader<T> }): Extension<T>;
// Warning: (ae-forgotten-export) The symbol "OptionalParams" needs to be exported by the entry point index.d.ts
@@ -284,7 +284,7 @@ export function createExternalRouteRef<
[param in ParamKey]: string;
},
Optional extends boolean = false,
ParamKey extends string = never
ParamKey extends string = never,
>(options: {
id: string;
params?: ParamKey[];
@@ -296,7 +296,7 @@ export function createExternalRouteRef<
// @public (undocumented)
export function createPlugin<
Routes extends AnyRoutes = {},
ExternalRoutes extends AnyExternalRoutes = {}
ExternalRoutes extends AnyExternalRoutes = {},
>(
config: PluginConfig<Routes, ExternalRoutes>,
): BackstagePlugin<Routes, ExternalRoutes>;
@@ -305,7 +305,7 @@ export function createPlugin<
//
// @public (undocumented)
export function createReactExtension<
T extends (props: any) => JSX.Element | null
T extends (props: any) => JSX.Element | null,
>(options: {
component: ComponentLoader<T>;
data?: Record<string, unknown>;
@@ -315,7 +315,7 @@ export function createReactExtension<
//
// @public (undocumented)
export function createRoutableExtension<
T extends (props: any) => JSX.Element | null
T extends (props: any) => JSX.Element | null,
>(options: { component: () => Promise<T>; mountPoint: RouteRef }): Extension<T>;
// Warning: (ae-missing-release-tag) "createRouteRef" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@@ -325,7 +325,7 @@ export function createRouteRef<
Params extends {
[param in ParamKey]: string;
},
ParamKey extends string = never
ParamKey extends string = never,
>(config: {
id?: string;
params?: ParamKey[];
@@ -342,7 +342,7 @@ export function createRouteRef<
// @public (undocumented)
export function createSubRouteRef<
Path extends string,
ParentParams extends AnyParams = never
ParentParams extends AnyParams = never,
>(config: {
id: string;
path: Path;
@@ -369,7 +369,7 @@ export interface ElementCollection {
getElements<
Props extends {
[name: string]: unknown;
}
},
>(): Array<ReactElement<Props>>;
selectByComponentData(query: {
key: string;
@@ -421,7 +421,7 @@ export type Extension<T> = {
// @public (undocumented)
export type ExternalRouteRef<
Params extends AnyParams = any,
Optional extends boolean = any
Optional extends boolean = any,
> = {
readonly [routeRefType]: 'external';
params: ParamKeys<Params>;
@@ -667,7 +667,7 @@ export type PendingAuthRequest = {
// @public (undocumented)
export type PluginConfig<
Routes extends AnyRoutes,
ExternalRoutes extends AnyExternalRoutes
ExternalRoutes extends AnyExternalRoutes,
> = {
id: string;
apis?: Iterable<AnyApiFactory>;
@@ -885,9 +885,7 @@ export function useRouteRefParams<Params extends AnyParams>(
// Warning: (ae-missing-release-tag) "withApis" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export function withApis<T>(
apis: TypesToApiRefs<T>,
): <P extends T>(
export function withApis<T>(apis: TypesToApiRefs<T>): <P extends T>(
WrappedComponent: React_2.ComponentType<P>,
) => {
(props: React_2.PropsWithChildren<Omit<P, keyof T>>): JSX.Element;
@@ -24,7 +24,7 @@ import { ApiRef, ApiFactory, TypesToApiRefs } from './types';
export function createApiFactory<
Api,
Impl extends Api,
Deps extends { [name in string]: unknown }
Deps extends { [name in string]: unknown },
>(factory: ApiFactory<Api, Impl, Deps>): ApiFactory<Api, Impl, Deps>;
export function createApiFactory<Api, Impl extends Api>(
api: ApiRef<Api>,
@@ -33,7 +33,7 @@ export function createApiFactory<Api, Impl extends Api>(
export function createApiFactory<
Api,
Impl extends Api,
Deps extends { [name in string]: unknown }
Deps extends { [name in string]: unknown },
>(
factory: ApiFactory<Api, Impl, Deps> | ApiRef<Api>,
instance?: Impl,
@@ -36,7 +36,7 @@ export type ApiHolder = {
export type ApiFactory<
Api,
Impl extends Api,
Deps extends { [name in string]: unknown }
Deps extends { [name in string]: unknown },
> = {
api: ApiRef<Api>;
deps: TypesToApiRefs<Deps>;
+2 -3
View File
@@ -18,9 +18,8 @@ import { useVersionedContext } from '../lib/versionedValues';
import { AppContext as AppContextV1 } from './types';
export const useApp = (): AppContextV1 => {
const versionedContext = useVersionedContext<{ 1: AppContextV1 }>(
'app-context',
);
const versionedContext =
useVersionedContext<{ 1: AppContextV1 }>('app-context');
const appContext = versionedContext.atVersion(1);
if (!appContext) {
throw new Error('AppContext v1 not available');
@@ -33,7 +33,7 @@ type ComponentLoader<T> =
// ComponentType inserts children as an optional prop whether the inner component accepts it or not,
// making it impossible to make the usage of children type safe.
export function createRoutableExtension<
T extends (props: any) => JSX.Element | null
T extends (props: any) => JSX.Element | null,
>(options: {
component: () => Promise<T>;
mountPoint: RouteRef;
@@ -91,7 +91,7 @@ export function createRoutableExtension<
// ComponentType inserts children as an optional prop whether the inner component accepts it or not,
// making it impossible to make the usage of children type safe.
export function createComponentExtension<
T extends (props: any) => JSX.Element | null
T extends (props: any) => JSX.Element | null,
>(options: { component: ComponentLoader<T> }): Extension<T> {
const { component } = options;
return createReactExtension({ component });
@@ -101,7 +101,7 @@ export function createComponentExtension<
// ComponentType inserts children as an optional prop whether the inner component accepts it or not,
// making it impossible to make the usage of children type safe.
export function createReactExtension<
T extends (props: any) => JSX.Element | null
T extends (props: any) => JSX.Element | null,
>(options: {
component: ComponentLoader<T>;
data?: Record<string, unknown>;
@@ -111,9 +111,9 @@ export function createReactExtension<
let Component: T;
if ('lazy' in options.component) {
const lazyLoader = options.component.lazy;
Component = (lazy(() =>
Component = lazy(() =>
lazyLoader().then(component => ({ default: component })),
) as unknown) as T;
) as unknown as T;
} else {
Component = options.component.sync;
}
@@ -32,7 +32,7 @@ export type VersionedValue<Versions extends { [version: number]: any }> = {
* Creates a container for a map of versioned values that implements VersionedValue.
*/
export function createVersionedValueMap<
Versions extends { [version: number]: any }
Versions extends { [version: number]: any },
>(versions: Versions): VersionedValue<Versions> {
Object.freeze(versions);
return {
@@ -43,7 +43,7 @@ export function createVersionedValueMap<
}
export function useVersionedContext<
Versions extends { [version in number]: any }
Versions extends { [version in number]: any },
>(key: string): VersionedValue<Versions> {
const versionedValue = useContext(
getGlobalSingleton<Context<VersionedValue<Versions>>>(key),
@@ -26,8 +26,9 @@ import { AnyApiFactory } from '../apis';
export class PluginImpl<
Routes extends AnyRoutes,
ExternalRoutes extends AnyExternalRoutes
> implements BackstagePlugin<Routes, ExternalRoutes> {
ExternalRoutes extends AnyExternalRoutes,
> implements BackstagePlugin<Routes, ExternalRoutes>
{
private storedOutput?: PluginOutput[];
constructor(private readonly config: PluginConfig<Routes, ExternalRoutes>) {}
@@ -81,7 +82,7 @@ export class PluginImpl<
export function createPlugin<
Routes extends AnyRoutes = {},
ExternalRoutes extends AnyExternalRoutes = {}
ExternalRoutes extends AnyExternalRoutes = {},
>(
config: PluginConfig<Routes, ExternalRoutes>,
): BackstagePlugin<Routes, ExternalRoutes> {
+2 -2
View File
@@ -42,7 +42,7 @@ export type AnyExternalRoutes = { [name: string]: ExternalRouteRef };
export type BackstagePlugin<
Routes extends AnyRoutes = {},
ExternalRoutes extends AnyExternalRoutes = {}
ExternalRoutes extends AnyExternalRoutes = {},
> = {
getId(): string;
output(): PluginOutput[];
@@ -54,7 +54,7 @@ export type BackstagePlugin<
export type PluginConfig<
Routes extends AnyRoutes,
ExternalRoutes extends AnyExternalRoutes
ExternalRoutes extends AnyExternalRoutes,
> = {
id: string;
apis?: Iterable<AnyApiFactory>;
@@ -24,8 +24,9 @@ import {
export class ExternalRouteRefImpl<
Params extends AnyParams,
Optional extends boolean
> implements ExternalRouteRef<Params, Optional> {
Optional extends boolean,
> implements ExternalRouteRef<Params, Optional>
{
readonly [routeRefType] = 'external';
constructor(
@@ -42,7 +43,7 @@ export class ExternalRouteRefImpl<
export function createExternalRouteRef<
Params extends { [param in ParamKey]: string },
Optional extends boolean = false,
ParamKey extends string = never
ParamKey extends string = never,
>(options: {
/**
* An identifier for this route, used to identify it in error messages
@@ -32,7 +32,8 @@ export type RouteRefConfig<Params extends AnyParams> = {
};
export class RouteRefImpl<Params extends AnyParams>
implements RouteRef<Params> {
implements RouteRef<Params>
{
readonly [routeRefType] = 'absolute';
constructor(
@@ -70,7 +71,7 @@ export function createRouteRef<
// ParamKey is here to make sure the Params type properly has its keys narrowed down
// to only the elements of params. Defaulting to never makes sure we end up with
// Param = {} if the params array is empty.
ParamKey extends string = never
ParamKey extends string = never,
>(config: {
/** The id of the route ref, used to identify it when printed */
id?: string;
@@ -27,7 +27,8 @@ import {
const PARAM_PATTERN = /^\w+$/;
export class SubRouteRefImpl<Params extends AnyParams>
implements SubRouteRef<Params> {
implements SubRouteRef<Params>
{
readonly [routeRefType] = 'sub';
constructor(
@@ -55,7 +56,7 @@ type PathParams<S extends string> = { [name in ParamNames<S>]: string };
*/
type MergeParams<
P1 extends { [param in string]: string },
P2 extends AnyParams
P2 extends AnyParams,
> = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2);
/**
@@ -64,14 +65,14 @@ type MergeParams<
*/
type MakeSubRouteRef<
Params extends { [param in string]: string },
ParentParams extends AnyParams
ParentParams extends AnyParams,
> = keyof Params & keyof ParentParams extends never
? SubRouteRef<OptionalParams<MergeParams<Params, ParentParams>>>
: never;
export function createSubRouteRef<
Path extends string,
ParentParams extends AnyParams = never
ParentParams extends AnyParams = never,
>(config: {
id: string;
path: Path;
@@ -21,9 +21,8 @@ export type AnyParams = { [param in string]: string } | undefined;
export type ParamKeys<Params extends AnyParams> = keyof Params extends never
? []
: (keyof Params)[];
export type OptionalParams<
Params extends { [param in string]: string }
> = Params[keyof Params] extends never ? undefined : Params;
export type OptionalParams<Params extends { [param in string]: string }> =
Params[keyof Params] extends never ? undefined : Params;
// The extra TS magic here is to require a single params argument if the RouteRef
// had at least one param defined, but require 0 arguments if there are no params defined.
@@ -65,7 +64,7 @@ export type SubRouteRef<Params extends AnyParams = any> = {
export type ExternalRouteRef<
Params extends AnyParams = any,
Optional extends boolean = any
Optional extends boolean = any,
> = {
readonly [routeRefType]: 'external';
@@ -48,9 +48,8 @@ export function useRouteRef<Params extends AnyParams>(
| ExternalRouteRef<Params, any>,
): RouteFunc<Params> | undefined {
const sourceLocation = useLocation();
const versionedContext = useVersionedContext<{ 1: RouteResolver }>(
'routing-context',
);
const versionedContext =
useVersionedContext<{ 1: RouteResolver }>('routing-context');
const resolver = versionedContext.atVersion(1);
const routeFunc = useMemo(
() => resolver && resolver.resolve(routeRef, sourceLocation),
-1
View File
@@ -39,7 +39,6 @@
"devDependencies": {
"@types/fs-extra": "^9.0.1",
"@types/inquirer": "^7.3.1",
"@types/react-dev-utils": "^9.0.4",
"@types/recursive-readdir": "^2.2.0",
"ts-node": "^10.0.0"
},
@@ -42,7 +42,7 @@ const AppRouter = app.getRouter();
const routes = (
<FlatRoutes>
<Navigate key="/" to="/catalog" />
<Navigate key="/" to="catalog" />
<Route path="/catalog" element={<CatalogIndexPage />} />
<Route
path="/catalog/:namespace/:kind/:name"
@@ -77,7 +77,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
<SidebarSearch />
<SidebarDivider />
{/* Global nav, not org-specific */}
<SidebarItem icon={HomeIcon} to="/catalog" text="Home" />
<SidebarItem icon={HomeIcon} to="catalog" text="Home" />
<SidebarItem icon={ExtensionIcon} to="api-docs" text="APIs" />
<SidebarItem icon={LibraryBooks} to="docs" text="Docs" />
<SidebarItem icon={CreateComponentIcon} to="create" text="Create..." />
+1 -1
View File
@@ -96,7 +96,7 @@ class DevAppBuilder {
registerApi<
Api,
Impl extends Api,
Deps extends { [name in string]: unknown }
Deps extends { [name in string]: unknown },
>(factory: ApiFactory<Api, Impl, Deps>): DevAppBuilder {
this.apis.push(factory);
return this;
@@ -186,8 +186,7 @@ describe('ApiDocGenerator', () => {
type: 'method',
name: 'z',
path: 'MyApiType.z',
text:
'z(a: Promise<readonly [{k: MySecondSubType}[]]>): Array<MyThirdSubType>',
text: 'z(a: Promise<readonly [{k: MySecondSubType}[]]>): Array<MyThirdSubType>',
docs: ['Multiple', 'JsDoc', 'Comments'],
links: [
{
@@ -127,9 +127,9 @@ export default class ApiDocGenerator {
);
const docs = this.getNodeDocs(declaration);
const membersAndTypes = Array.from(
interfaceMembers.values(),
).flatMap(fieldSymbol => this.getMemberInfo(name, fieldSymbol));
const membersAndTypes = Array.from(interfaceMembers.values()).flatMap(
fieldSymbol => this.getMemberInfo(name, fieldSymbol),
);
const members = membersAndTypes.map(t => t.member);
const dependentTypes = this.flattenTypes(
@@ -174,9 +174,8 @@ export default class ApiDocGenerator {
): { member: FieldInfo; dependentTypes: TypeInfo[] } {
const declaration = symbol.valueDeclaration;
const { links, infos: dependentTypes } = this.findAllTypeReferences(
declaration,
);
const { links, infos: dependentTypes } =
this.findAllTypeReferences(declaration);
let type: FieldInfo['type'] = 'prop';
if (

Some files were not shown because too many files have changed in this diff Show More