Merge branch 'backstage:master' into feature/support-aoss
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs-backend': minor
|
||||
'@backstage/plugin-techdocs-node': minor
|
||||
---
|
||||
|
||||
Allow prepared directory clean up for custom preparers
|
||||
|
||||
When using custom preparer for TechDocs, the `preparedDir` might
|
||||
end up taking disk space. This requires all custom preparers to
|
||||
implement a new method `shouldCleanPreparedDirectory` which indicates
|
||||
whether the prepared directory should be cleaned after generation.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': minor
|
||||
---
|
||||
|
||||
Extension attachment point is now configured via `attachTo: { id, input }` instead of `at: 'id/input'`.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': minor
|
||||
'@backstage/frontend-app-api': minor
|
||||
---
|
||||
|
||||
Removed support for the new `useRouteRef`.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Improved `DocsTable` to display pagination controls dynamically, appearing only when needed.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Implement `toString()` and `toJSON()` for extension instances.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-newrelic': patch
|
||||
---
|
||||
|
||||
Fixed sorting and searching in the NewRelic table.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-adr': patch
|
||||
---
|
||||
|
||||
Fix icon alignment in `AdrSearchResultListItem`
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Added `/testUtils` entry point, with a utility for mocking resolve package paths as returned by `resolvePackagePath`.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': patch
|
||||
'@backstage/plugin-graphiql': patch
|
||||
---
|
||||
|
||||
Updated `/alpha` exports to use new `attachTo` option.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Updates for `at` -> `attachTo` refactor.
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
node-version: [18.x, 20.x]
|
||||
|
||||
env:
|
||||
CI: true
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
node-version: [18.x, 20.x]
|
||||
|
||||
env:
|
||||
CI: true
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
node-version: [18.x, 20.x]
|
||||
|
||||
name: Test ${{ matrix.node-version }}
|
||||
services:
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [18.x]
|
||||
node-version: [18.x, 20.x]
|
||||
|
||||
services:
|
||||
postgres13:
|
||||
|
||||
@@ -260,4 +260,5 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/
|
||||
| [Localiza&Co](https://www.localiza.com/) | [Augusto Amormino](https://github.com/augustoamormino), [Jonas Soares](https://github.com/jonaopower), [Alexandre Amormino](https://github.com/alexandreamormino), [Greg Almeida](https://github.com/sephh) | We're excited to announce our adoption of Backstage as our Internal Developer Portal! Our mission is to elevate the Developer Experience by streaming information access. Backstage will serve as the ultimate hub for developer resources, including documentation, tools, software insights, and metrics. Through Backstage, we're simplifying processes, offering software templates to empowered and efficient development journey, enhancing self-service capabilities with the embedded all best practices. |
|
||||
| [V2 Digital](https://v2.digital) | [Joe Patterson](https://github.com/jrwpatterson)| We will be using it to be a corporate dashboard plus our software catalog. |
|
||||
| [AppsFlyer](https://www.appsflyer.com/) | [Shahar Shmaram](https://github.com/shmaram) | Internal Developer Portal, a catalog of all company resources, custom providers and processors, scaffolder for generating new resources.
|
||||
| [Gynzy](https://gynzy.com) | [Stef Louwers](https://github.com/fhp) | We are building an internal developer portal to get an overview of all our software components. |
|
||||
| [Cielo](https://www.cielo.com.br) | [@Alex Silva](https://github.com/narokwq) | We are using as our Internal Developer Portal, it provides developers with the resources, information, and tools they need to build high-quality applications and adhere to organizational standards and security practices. |
|
||||
|
||||
@@ -22,12 +22,14 @@ Team: @backstage/catalog-maintainers
|
||||
|
||||
Scope: The catalog plugin and catalog model
|
||||
|
||||
| Name | Organization | Team | GitHub | Discord |
|
||||
| -------------- | ------------ | --------- | ---------------------------------------- | ---------------- |
|
||||
| Rickard Dybeck | Spotify | Chipmunks | [alde](http://github.com/alde) | rdybeck#8083 |
|
||||
| Mike Blockley | Spotify | Chipmunks | [mikeyhc](http://github.com/mikeyhc) | mikey-spot#5363 |
|
||||
| Elon Jefferson | Spotify | Chipmunks | [Edje-C](http://github.com/Edje-C) | elon-spotty#6086 |
|
||||
| Nurit Izrailov | Spotify | Chipmunks | [nuritizra](http://github.com/nuritizra) | - |
|
||||
| Name | Organization | Team | GitHub | Discord |
|
||||
| --------------- | ------------ | --------- | ----------------------------------------- | ------------------- |
|
||||
| Rickard Dybeck | Spotify | Chipmunks | [alde](https://github.com/alde) | `rdybeck#8083` |
|
||||
| Mike Blockley | Spotify | Chipmunks | [mikeyhc](https://github.com/mikeyhc) | `mikey-spot#5363` |
|
||||
| Elon Jefferson | Spotify | Chipmunks | [Edje-C](https://github.com/Edje-C) | `elon-spotty#6086 ` |
|
||||
| Nurit Izrailov | Spotify | Chipmunks | [nuritizra](https://github.com/nuritizra) | - |
|
||||
| Hunter Dougless | Spotify | Chipmunks | [hntrdglss](https://github.com/hntrdglss) | `hntrdglss#1849` |
|
||||
| Seve Kim | Spotify | Chipmunks | [sevedkim](https://github.com/sevedkim) | `seve#9951` |
|
||||
|
||||
### Discoverability
|
||||
|
||||
@@ -118,6 +120,17 @@ Scope: Tooling for frontend and backend schema-first OpenAPI development.
|
||||
| -------------- | ------------ | --------------------------------------- | ------------- |
|
||||
| Aramis Sennyey | Spotify | [sennyeya](https://github.com/sennyeya) | `Aramis#7984` |
|
||||
|
||||
### Scaffolder
|
||||
|
||||
Team: @backstage/scaffolder-maintainers
|
||||
|
||||
Scope: The Scaffolder frontend and backend plugins, and related tooling.
|
||||
|
||||
| Name | Organization | GitHub | Discord |
|
||||
| ------------------- | -------------- | ------------------------------------- | ---------------- |
|
||||
| Bogdan Nechyporenko | Bol.com | [acierto](https://github.com/acierto) | `bogdan_haarlem` |
|
||||
| Paul Cowan | frontendrescue | [dagda1](https://github.com/dadga1) | `dagda1` |
|
||||
|
||||
## Sponsors
|
||||
|
||||
| Name | Organization | GitHub | Email |
|
||||
|
||||
@@ -65,7 +65,7 @@ page in your `App.tsx`:
|
||||
// packages/app/src/App.tsx
|
||||
|
||||
import { TechDocsReaderPage } from '@backstage/plugin-techdocs';
|
||||
import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha';
|
||||
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
|
||||
import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
|
||||
|
||||
// ...
|
||||
@@ -99,7 +99,7 @@ is very similar; instead of adding the `<TechDocsAddons>` registry under a
|
||||
|
||||
import { EntityLayout } from '@backstage/plugin-catalog';
|
||||
import { EntityTechdocsContent } from '@backstage/plugin-techdocs';
|
||||
import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha';
|
||||
import { TechDocsAddons } from '@backstage/plugin-techdocs-react';
|
||||
import { ReportIssue } from '@backstage/plugin-techdocs-module-addons-contrib';
|
||||
|
||||
// ...
|
||||
@@ -146,7 +146,7 @@ an Addon, follow these steps:
|
||||
import {
|
||||
createTechDocsAddonExtension,
|
||||
TechDocsAddonLocations,
|
||||
} from '@backstage/plugin-techdocs-react/alpha';
|
||||
} from '@backstage/plugin-techdocs-react';
|
||||
import { CatGifComponent, CatGifComponentProps } from './addons';
|
||||
|
||||
// ...
|
||||
@@ -179,7 +179,7 @@ provided by the Addon framework.
|
||||
// plugins/your-plugin/src/addons/MakeAllImagesCatGifs.tsx
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { useShadowRootElements } from '@backstage/plugin-techdocs-react/alpha';
|
||||
import { useShadowRootElements } from '@backstage/plugin-techdocs-react';
|
||||
|
||||
// This is a normal react component; in order to make it an Addon, you would
|
||||
// still create and provide it via your plugin as described above. The only
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Litmus
|
||||
author: litmuschaos.io
|
||||
authorUrl: https://github.com/litmuschaos/backstage-plugin
|
||||
category: Chaos Engineering
|
||||
description: This plugin lets you view the status of Litmus resources and launch Chaos Experiments directly inside Backstage.
|
||||
documentation: https://github.com/litmuschaos/backstage-plugin/blob/master/README.md
|
||||
iconUrl: https://raw.githubusercontent.com/cncf/artwork/master/projects/litmus/icon/color/litmus-icon-color.svg
|
||||
npmPackageName: 'backstage-plugin-litmus'
|
||||
addedDate: '2023-10-06'
|
||||
@@ -3,7 +3,7 @@ title: Pulumi
|
||||
author: Pulumi
|
||||
authorUrl: https://www.pulumi.com
|
||||
category: Infrastructure
|
||||
description: View Pulumi stack information in Backstage.
|
||||
description: Use Pulumi scaffolder actions and view Pulumi stack information in Backstage.
|
||||
documentation: https://github.com/pulumi/pulumi-backstage-plugin
|
||||
iconUrl: https://www.pulumi.com/logos/brand/avatar-on-white.png
|
||||
npmPackageName: '@pulumi/backstage-plugin-pulumi'
|
||||
|
||||
+3
-3
@@ -10317,15 +10317,15 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"sass@npm:^1.57.1":
|
||||
version: 1.66.1
|
||||
resolution: "sass@npm:1.66.1"
|
||||
version: 1.69.0
|
||||
resolution: "sass@npm:1.69.0"
|
||||
dependencies:
|
||||
chokidar: ">=3.0.0 <4.0.0"
|
||||
immutable: ^4.0.0
|
||||
source-map-js: ">=0.6.2 <2.0.0"
|
||||
bin:
|
||||
sass: sass.js
|
||||
checksum: 74fc11d0fcd5e16c5331b57dd59865705a299c64e89f2b99646869caeb011dc8d0b6144a6c74a90c264e9ef70654207dbf44fc9b7e3393f8bd14809b904c8a52
|
||||
checksum: eabea31ea3b1dd529c7eff345c8b6468afe6ab8011bd4f95caa2cffb8fb115cc055ea21de425be6197f7ed22516f5652eccb98d315d592ea152ada553f964b8a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
@@ -19,9 +19,8 @@ import { Link } from '@backstage/core-components';
|
||||
import {
|
||||
createPageExtension,
|
||||
createPlugin,
|
||||
useRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { useRouteRef, createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
const indexRouteRef = createRouteRef({ id: 'index' });
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
"@types/node-forge": "^1.3.0",
|
||||
"@types/stoppable": "^1.1.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"supertest": "^6.1.3"
|
||||
},
|
||||
"configSchema": "config.d.ts",
|
||||
|
||||
@@ -14,28 +14,31 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import mockFs from 'mock-fs';
|
||||
import { resolve as resolvePath, dirname } from 'path';
|
||||
import { startTestBackend, mockServices } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
startTestBackend,
|
||||
mockServices,
|
||||
createMockDirectory,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { featureDiscoveryServiceFactory } from './featureDiscoveryServiceFactory';
|
||||
|
||||
const rootDir = dirname(process.argv[1]);
|
||||
const mockDir = createMockDirectory();
|
||||
process.argv[1] = mockDir.path;
|
||||
|
||||
const pluginApiPath = require.resolve('@backstage/backend-plugin-api');
|
||||
|
||||
describe('featureDiscoveryServiceFactory', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[rootDir]: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'example-app',
|
||||
dependencies: {
|
||||
'detected-plugin': '0.0.0',
|
||||
'detected-module': '0.0.0',
|
||||
'detected-plugin-with-alpha': '0.0.0',
|
||||
'detected-library': '0.0.0',
|
||||
},
|
||||
}),
|
||||
},
|
||||
[resolvePath(rootDir, 'node_modules/detected-plugin')]: {
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
name: 'example-app',
|
||||
dependencies: {
|
||||
'detected-plugin': '0.0.0',
|
||||
'detected-module': '0.0.0',
|
||||
'detected-plugin-with-alpha': '0.0.0',
|
||||
'detected-library': '0.0.0',
|
||||
},
|
||||
}),
|
||||
'node_modules/detected-plugin': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'detected-plugin',
|
||||
main: 'index.js',
|
||||
@@ -44,7 +47,7 @@ describe('featureDiscoveryServiceFactory', () => {
|
||||
},
|
||||
}),
|
||||
'index.js': `
|
||||
const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api');
|
||||
const { createBackendPlugin, coreServices } = require('${pluginApiPath}');
|
||||
exports.default = createBackendPlugin({
|
||||
pluginId: 'detected',
|
||||
register(env) {
|
||||
@@ -58,7 +61,7 @@ describe('featureDiscoveryServiceFactory', () => {
|
||||
});
|
||||
`,
|
||||
},
|
||||
[resolvePath(rootDir, 'node_modules/detected-module')]: {
|
||||
'node_modules/detected-module': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'detected-module',
|
||||
main: 'index.js',
|
||||
@@ -67,7 +70,7 @@ describe('featureDiscoveryServiceFactory', () => {
|
||||
},
|
||||
}),
|
||||
'index.js': `
|
||||
const { createBackendModule, coreServices } = require('@backstage/backend-plugin-api');
|
||||
const { createBackendModule, coreServices } = require('${pluginApiPath}');
|
||||
exports.default = createBackendModule({
|
||||
pluginId: 'detected',
|
||||
moduleId: 'derp',
|
||||
@@ -82,7 +85,7 @@ describe('featureDiscoveryServiceFactory', () => {
|
||||
});
|
||||
`,
|
||||
},
|
||||
[resolvePath(rootDir, 'node_modules/detected-plugin-with-alpha')]: {
|
||||
'node_modules/detected-plugin-with-alpha': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'detected-plugin-with-alpha',
|
||||
main: 'index.js',
|
||||
@@ -101,7 +104,7 @@ describe('featureDiscoveryServiceFactory', () => {
|
||||
}),
|
||||
'index.js': `exports.default = undefined;`,
|
||||
'alpha.js': `
|
||||
const { createBackendPlugin, coreServices } = require('@backstage/backend-plugin-api');
|
||||
const { createBackendPlugin, coreServices } = require('${pluginApiPath}');
|
||||
exports.default = createBackendPlugin({
|
||||
pluginId: 'detected-alpha',
|
||||
register(env) {
|
||||
@@ -115,7 +118,7 @@ describe('featureDiscoveryServiceFactory', () => {
|
||||
});
|
||||
`,
|
||||
},
|
||||
[resolvePath(rootDir, 'node_modules/detected-library')]: {
|
||||
'node_modules/detected-library': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'detected-library',
|
||||
main: 'index.js',
|
||||
@@ -124,7 +127,7 @@ describe('featureDiscoveryServiceFactory', () => {
|
||||
},
|
||||
}),
|
||||
'index.js': `
|
||||
const { createServiceFactory, createServiceRef, coreServices } = require('@backstage/backend-plugin-api');
|
||||
const { createServiceFactory, createServiceRef, coreServices } = require('${pluginApiPath}');
|
||||
exports.default = createServiceFactory({
|
||||
service: createServiceRef({ id: 'test', scope: 'root' }),
|
||||
deps: { logger: coreServices.rootLogger },
|
||||
@@ -138,10 +141,6 @@ describe('featureDiscoveryServiceFactory', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should detect plugin and module packages when "all" is specified', async () => {
|
||||
const mock = mockServices.rootLogger.mock({ child: () => mock });
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./alpha": "./src/alpha.ts",
|
||||
"./testUtils": "./src/testUtils.ts",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
@@ -17,6 +18,9 @@
|
||||
"alpha": [
|
||||
"src/alpha.ts"
|
||||
],
|
||||
"testUtils": [
|
||||
"src/testUtils.ts"
|
||||
],
|
||||
"package.json": [
|
||||
"package.json"
|
||||
]
|
||||
|
||||
@@ -18,6 +18,12 @@ import { isChildPath } from '@backstage/cli-common';
|
||||
import { NotAllowedError } from '@backstage/errors';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
|
||||
/** @internal */
|
||||
export const packagePathMocks = new Map<
|
||||
string,
|
||||
(paths: string[]) => string | undefined
|
||||
>();
|
||||
|
||||
/**
|
||||
* Resolve a path relative to the root of a package directory.
|
||||
* Additional path arguments are resolved relative to the package dir.
|
||||
@@ -29,6 +35,14 @@ import { resolve as resolvePath } from 'path';
|
||||
* @public
|
||||
*/
|
||||
export function resolvePackagePath(name: string, ...paths: string[]) {
|
||||
const mockedResolve = packagePathMocks.get(name);
|
||||
if (mockedResolve) {
|
||||
const resolved = mockedResolve(paths);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
|
||||
const req =
|
||||
typeof __non_webpack_require__ === 'undefined'
|
||||
? require
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2023 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 { packagePathMocks } from './paths';
|
||||
import { posix as posixPath, resolve as resolvePath } from 'path';
|
||||
|
||||
/** @public */
|
||||
export interface PackagePathResolutionOverride {
|
||||
/** Restores the normal behavior of resolvePackagePath */
|
||||
restore(): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface OverridePackagePathResolutionOptions {
|
||||
/** The name of the package to mock the resolved path of */
|
||||
packageName: string;
|
||||
|
||||
/** A replacement for the root package path */
|
||||
path?: string;
|
||||
|
||||
/**
|
||||
* Replacements for package sub-paths, each key must be an exact match of the posix-style path
|
||||
* that is being resolved within the package.
|
||||
*
|
||||
* For example, code calling `resolvePackagePath('x', 'foo', 'bar')` would match only the following
|
||||
* configuration: `overridePackagePathResolution({ packageName: 'x', paths: { 'foo/bar': baz } })`
|
||||
*/
|
||||
paths?: { [path in string]: string | (() => string) };
|
||||
}
|
||||
|
||||
/**
|
||||
* This utility helps you override the paths returned by `resolvePackagePath` for a given package.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function overridePackagePathResolution(
|
||||
options: OverridePackagePathResolutionOptions,
|
||||
): PackagePathResolutionOverride {
|
||||
const name = options.packageName;
|
||||
|
||||
if (packagePathMocks.has(name)) {
|
||||
throw new Error(
|
||||
`Tried to override resolution for '${name}' more than once for package '${name}'`,
|
||||
);
|
||||
}
|
||||
|
||||
packagePathMocks.set(name, paths => {
|
||||
const joinedPath = posixPath.join(...paths);
|
||||
const localResolver = options.paths?.[joinedPath];
|
||||
if (localResolver) {
|
||||
return typeof localResolver === 'function'
|
||||
? localResolver()
|
||||
: localResolver;
|
||||
}
|
||||
if (options.path) {
|
||||
return resolvePath(options.path, ...paths);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
return {
|
||||
restore() {
|
||||
packagePathMocks.delete(name);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
## API Report File for "@backstage/backend-common"
|
||||
|
||||
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
|
||||
|
||||
```ts
|
||||
// @public
|
||||
export function overridePackagePathResolution(
|
||||
options: OverridePackagePathResolutionOptions,
|
||||
): PackagePathResolutionOverride;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface OverridePackagePathResolutionOptions {
|
||||
packageName: string;
|
||||
path?: string;
|
||||
paths?: {
|
||||
[path in string]: string | (() => string);
|
||||
};
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface PackagePathResolutionOverride {
|
||||
restore(): void;
|
||||
}
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
@@ -55,7 +55,6 @@
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/config-loader": "workspace:^",
|
||||
"mock-fs": "^5.2.0",
|
||||
"wait-for-expect": "^3.0.2"
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -20,10 +20,9 @@ import {
|
||||
coreServices,
|
||||
createServiceFactory,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import mockFs, { directory, symlink } from 'mock-fs';
|
||||
import * as path from 'path';
|
||||
import * as url from 'url';
|
||||
|
||||
import fs from 'fs';
|
||||
import {
|
||||
BackendDynamicPlugin,
|
||||
BaseDynamicPlugin,
|
||||
@@ -43,11 +42,13 @@ import { ConfigSources } from '@backstage/config-loader';
|
||||
import { Logs, MockedLogger, LogContent } from '../__testUtils__/testUtils';
|
||||
import { PluginScanner } from '../scanner/plugin-scanner';
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('backend-plugin-manager', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
describe('loadPlugins', () => {
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
@@ -56,7 +57,7 @@ describe('backend-plugin-manager', () => {
|
||||
packageManifest: ScannedPluginManifest;
|
||||
indexFile?: {
|
||||
retativePath: string[];
|
||||
content?: string;
|
||||
content: string;
|
||||
};
|
||||
expectedLogs?(location: URL): {
|
||||
errors?: LogContent[];
|
||||
@@ -354,17 +355,13 @@ describe('backend-plugin-manager', () => {
|
||||
},
|
||||
])('$name', async (tc: TestCase): Promise<void> => {
|
||||
const plugin: ScannedPluginPackage = {
|
||||
location: url.pathToFileURL(
|
||||
path.resolve(`/node_modules/jest-tests/${randomUUID()}`),
|
||||
),
|
||||
location: url.pathToFileURL(mockDir.resolve(randomUUID())),
|
||||
manifest: tc.packageManifest,
|
||||
};
|
||||
|
||||
const mockedFiles = {
|
||||
[path.join(url.fileURLToPath(plugin.location), 'package.json')]:
|
||||
mockFs.file({
|
||||
content: JSON.stringify(plugin),
|
||||
}),
|
||||
JSON.stringify(plugin),
|
||||
};
|
||||
if (tc.indexFile) {
|
||||
mockedFiles[
|
||||
@@ -372,11 +369,9 @@ describe('backend-plugin-manager', () => {
|
||||
url.fileURLToPath(plugin.location),
|
||||
...tc.indexFile.retativePath,
|
||||
)
|
||||
] = mockFs.file({
|
||||
content: tc.indexFile.content,
|
||||
});
|
||||
] = tc.indexFile.content;
|
||||
}
|
||||
mockFs(mockedFiles);
|
||||
mockDir.setContent(mockedFiles);
|
||||
|
||||
const logger = new MockedLogger();
|
||||
const pluginManager = new (PluginManager as any)(logger, [plugin], {
|
||||
@@ -440,8 +435,11 @@ describe('backend-plugin-manager', () => {
|
||||
});
|
||||
|
||||
describe('dynamicPluginsServiceFactory', () => {
|
||||
const otherMockDir = createMockDirectory();
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
mockDir.clear();
|
||||
otherMockDir.clear();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
@@ -449,14 +447,16 @@ describe('backend-plugin-manager', () => {
|
||||
const logger = new MockedLogger();
|
||||
const rootLogger = new MockedLogger();
|
||||
|
||||
mockFs({
|
||||
[findPaths(__dirname).resolveTargetRoot('package.json')]: mockFs.load(
|
||||
mockDir.setContent({
|
||||
'package.json': fs.readFileSync(
|
||||
findPaths(__dirname).resolveTargetRoot('package.json'),
|
||||
),
|
||||
'/somewhere/dynamic-plugins-root/a-dynamic-plugin': symlink({
|
||||
path: '/somewhere-else/a-dynamic-plugin',
|
||||
}),
|
||||
'/somewhere-else/a-dynamic-plugin': directory({}),
|
||||
'dynamic-plugins-root': {},
|
||||
'dynamic-plugins-root/a-dynamic-plugin': ctx =>
|
||||
ctx.symlink(otherMockDir.resolve('a-dynamic-plugin')),
|
||||
});
|
||||
otherMockDir.setContent({
|
||||
'a-dynamic-plugin': {},
|
||||
});
|
||||
|
||||
const fromConfigSpier = jest.spyOn(PluginManager, 'fromConfig');
|
||||
@@ -468,7 +468,7 @@ describe('backend-plugin-manager', () => {
|
||||
.mockImplementation(async () => [
|
||||
{
|
||||
location: url.pathToFileURL(
|
||||
path.resolve('/somewhere/dynamic-plugins-root/a-dynamic-plugin'),
|
||||
mockDir.resolve('dynamic-plugins-root/a-dynamic-plugin'),
|
||||
),
|
||||
manifest: {
|
||||
name: 'test',
|
||||
@@ -533,11 +533,11 @@ describe('backend-plugin-manager', () => {
|
||||
expect(scanRootSpier).toHaveBeenCalled();
|
||||
expect(mockedModuleLoader.bootstrap).toHaveBeenCalledWith(
|
||||
findPaths(__dirname).targetRoot,
|
||||
[path.resolve('/somewhere-else/a-dynamic-plugin')],
|
||||
[fs.realpathSync(otherMockDir.resolve('a-dynamic-plugin'))],
|
||||
);
|
||||
expect(mockedModuleLoader.load).toHaveBeenCalledWith(
|
||||
path.resolve(
|
||||
'/somewhere/dynamic-plugins-root/a-dynamic-plugin/dist/index.cjs.js',
|
||||
mockDir.resolve(
|
||||
'dynamic-plugins-root/a-dynamic-plugin/dist/index.cjs.js',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -15,13 +15,18 @@
|
||||
*/
|
||||
|
||||
import { PluginScanner } from './plugin-scanner';
|
||||
import mockFs from 'mock-fs';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { Logs, MockedLogger } from '../__testUtils__/testUtils';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import path from 'path';
|
||||
import * as url from 'url';
|
||||
import { ScannedPluginPackage } from './types';
|
||||
import {
|
||||
MockDirectoryContent,
|
||||
createMockDirectory,
|
||||
} from '@backstage/backend-test-utils';
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
describe('plugin-scanner', () => {
|
||||
const env = process.env;
|
||||
@@ -30,7 +35,7 @@ describe('plugin-scanner', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
mockDir.clear();
|
||||
process.env = env;
|
||||
});
|
||||
|
||||
@@ -61,85 +66,77 @@ describe('plugin-scanner', () => {
|
||||
},
|
||||
{
|
||||
name: 'valid config with relative root directory path',
|
||||
backstageRoot: '/backstageRoot',
|
||||
backstageRoot: mockDir.resolve('backstageRoot'),
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory(),
|
||||
},
|
||||
}),
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {},
|
||||
},
|
||||
},
|
||||
config: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: 'dist-dynamic',
|
||||
},
|
||||
},
|
||||
expectedRootDirectory: path.resolve('/backstageRoot/dist-dynamic'),
|
||||
expectedRootDirectory: mockDir.resolve('backstageRoot/dist-dynamic'),
|
||||
},
|
||||
{
|
||||
name: 'valid config with absolute root directory path inside the backstage root',
|
||||
backstageRoot: '/backstageRoot',
|
||||
backstageRoot: mockDir.resolve('backstageRoot'),
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory(),
|
||||
},
|
||||
}),
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {},
|
||||
},
|
||||
},
|
||||
config: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: '/backstageRoot/dist-dynamic',
|
||||
rootDirectory: mockDir.resolve('backstageRoot/dist-dynamic'),
|
||||
},
|
||||
},
|
||||
expectedRootDirectory: path.resolve('/backstageRoot/dist-dynamic'),
|
||||
expectedRootDirectory: mockDir.resolve('backstageRoot/dist-dynamic'),
|
||||
},
|
||||
{
|
||||
name: 'valid config with absolute root directory path outside the backstage root',
|
||||
backstageRoot: '/backstageRoot',
|
||||
backstageRoot: mockDir.resolve('backstageRoot'),
|
||||
fileSystem: {
|
||||
'/somewhere': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory(),
|
||||
},
|
||||
}),
|
||||
somewhere: {
|
||||
'dist-dynamic': {},
|
||||
},
|
||||
},
|
||||
config: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: '/somewhere/dist-dynamic',
|
||||
rootDirectory: mockDir.resolve('somewhere/dist-dynamic'),
|
||||
},
|
||||
},
|
||||
expectedError: `Dynamic plugins under '${path.resolve(
|
||||
'/somewhere/dist-dynamic',
|
||||
)}' cannot access backstage modules in '${path.resolve(
|
||||
'/backstageRoot/node_modules',
|
||||
expectedError: `Dynamic plugins under '${mockDir.resolve(
|
||||
'somewhere/dist-dynamic',
|
||||
)}' cannot access backstage modules in '${mockDir.resolve(
|
||||
'backstageRoot/node_modules',
|
||||
)}'.
|
||||
Please add '${path.resolve(
|
||||
'/backstageRoot/node_modules',
|
||||
Please add '${mockDir.resolve(
|
||||
'backstageRoot/node_modules',
|
||||
)}' to the 'NODE_PATH' when running the backstage backend.`,
|
||||
},
|
||||
{
|
||||
name: 'valid config with absolute root directory path outside the backstage root but with backstage root included in NODE_PATH',
|
||||
backstageRoot: '/backstageRoot',
|
||||
backstageRoot: mockDir.resolve('backstageRoot'),
|
||||
fileSystem: {
|
||||
'/somewhere': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory(),
|
||||
},
|
||||
}),
|
||||
somewhere: {
|
||||
'dist-dynamic': {},
|
||||
},
|
||||
},
|
||||
config: {
|
||||
dynamicPlugins: {
|
||||
rootDirectory: '/somewhere/dist-dynamic',
|
||||
rootDirectory: mockDir.resolve('somewhere/dist-dynamic'),
|
||||
},
|
||||
},
|
||||
environment: {
|
||||
NODE_PATH: `${path.resolve('/somewhere-else')}${
|
||||
NODE_PATH: `${mockDir.resolve('somewhere-else')}${
|
||||
path.delimiter
|
||||
}${path.resolve('/backstageRoot', 'node_modules')}${
|
||||
}${mockDir.resolve('backstageRoot', 'node_modules')}${
|
||||
path.delimiter
|
||||
}${path.resolve('anywhere-else')}`,
|
||||
}${mockDir.resolve('anywhere-else')}`,
|
||||
},
|
||||
expectedRootDirectory: path.resolve('/somewhere/dist-dynamic'),
|
||||
expectedRootDirectory: mockDir.resolve('somewhere/dist-dynamic'),
|
||||
},
|
||||
{
|
||||
name: 'invalid config: dynamicPlugins not an object',
|
||||
@@ -186,13 +183,11 @@ Please add '${path.resolve(
|
||||
},
|
||||
{
|
||||
name: 'valid config pointing to a file instead of a directory',
|
||||
backstageRoot: '/backstageRoot',
|
||||
backstageRoot: mockDir.resolve('backstageRoot'),
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.file(),
|
||||
},
|
||||
}),
|
||||
backstageRoot: {
|
||||
'dist-dynamic': '',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
dynamicPlugins: {
|
||||
@@ -218,7 +213,7 @@ Please add '${path.resolve(
|
||||
);
|
||||
}
|
||||
if (tc.fileSystem) {
|
||||
mockFs(tc.fileSystem);
|
||||
mockDir.setContent(tc.fileSystem);
|
||||
}
|
||||
if (tc.expectedError) {
|
||||
/* eslint-disable-next-line jest/no-conditional-expect */
|
||||
@@ -240,7 +235,7 @@ Please add '${path.resolve(
|
||||
type TestCase = {
|
||||
name: string;
|
||||
preferAlpha?: boolean;
|
||||
fileSystem?: any;
|
||||
fileSystem?: MockDirectoryContent;
|
||||
expectedLogs?: Logs;
|
||||
expectedPluginPackages?: ScannedPluginPackage[];
|
||||
expectedError?: string;
|
||||
@@ -261,31 +256,23 @@ Please add '${path.resolve(
|
||||
{
|
||||
name: 'manifest found in directory',
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin': mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {
|
||||
'test-backend-plugin': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expectedPluginPackages: [
|
||||
{
|
||||
location: url.pathToFileURL(
|
||||
path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'),
|
||||
mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'),
|
||||
),
|
||||
manifest: {
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
@@ -299,38 +286,29 @@ Please add '${path.resolve(
|
||||
{
|
||||
name: 'backend plugin found in symlink',
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin': mockFs.symlink({
|
||||
path: '/somewhere-else/test-backend-plugin-target',
|
||||
}),
|
||||
},
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {
|
||||
'test-backend-plugin': ctx =>
|
||||
ctx.symlink(
|
||||
mockDir.resolve('somewhere-else/test-backend-plugin-target'),
|
||||
),
|
||||
},
|
||||
},
|
||||
'somewhere-else': {
|
||||
'test-backend-plugin-target': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
},
|
||||
}),
|
||||
'/somewhere-else': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin-target': mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expectedPluginPackages: [
|
||||
{
|
||||
location: url.pathToFileURL(
|
||||
path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'),
|
||||
mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'),
|
||||
),
|
||||
manifest: {
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
@@ -344,22 +322,18 @@ Please add '${path.resolve(
|
||||
{
|
||||
name: 'ignored folder child: not a directory',
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin': mockFs.file({}),
|
||||
},
|
||||
}),
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {
|
||||
'test-backend-plugin': '',
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expectedPluginPackages: [],
|
||||
expectedLogs: {
|
||||
infos: [
|
||||
{
|
||||
message: `skipping '${path.resolve(
|
||||
'/backstageRoot/dist-dynamic/test-backend-plugin',
|
||||
message: `skipping '${mockDir.resolve(
|
||||
'backstageRoot/dist-dynamic/test-backend-plugin',
|
||||
)}' since it is not a directory`,
|
||||
},
|
||||
],
|
||||
@@ -368,29 +342,24 @@ Please add '${path.resolve(
|
||||
{
|
||||
name: 'ignored folder child symlink: target is not a directory',
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin': mockFs.symlink({
|
||||
path: '/somewhere-else/test-backend-plugin-target',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {
|
||||
'test-backend-plugin': ctx =>
|
||||
ctx.symlink(
|
||||
mockDir.resolve('somewhere-else/test-backend-plugin-target'),
|
||||
),
|
||||
},
|
||||
}),
|
||||
'/somewhere-else': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin-target': mockFs.file({}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
'somewhere-else': {
|
||||
'test-backend-plugin-target': '',
|
||||
},
|
||||
},
|
||||
expectedPluginPackages: [],
|
||||
expectedLogs: {
|
||||
infos: [
|
||||
{
|
||||
message: `skipping '${path.resolve(
|
||||
'/backstageRoot/dist-dynamic/test-backend-plugin',
|
||||
message: `skipping '${mockDir.resolve(
|
||||
'backstageRoot/dist-dynamic/test-backend-plugin',
|
||||
)}' since it is not a directory`,
|
||||
},
|
||||
],
|
||||
@@ -400,42 +369,30 @@ Please add '${path.resolve(
|
||||
name: 'alpha manifest available but not preferred',
|
||||
preferAlpha: false,
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin': mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
}),
|
||||
alpha: mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: '../dist/alpha.cjs.js',
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {
|
||||
'test-backend-plugin': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
alpha: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: '../dist/alpha.cjs.js',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expectedPluginPackages: [
|
||||
{
|
||||
location: url.pathToFileURL(
|
||||
path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'),
|
||||
mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'),
|
||||
),
|
||||
manifest: {
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
@@ -450,43 +407,31 @@ Please add '${path.resolve(
|
||||
name: 'alpha manifest preferred and found in directory',
|
||||
preferAlpha: true,
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin': mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
}),
|
||||
alpha: mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: '../dist/alpha.cjs.js',
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {
|
||||
'test-backend-plugin': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
alpha: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: '../dist/alpha.cjs.js',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expectedPluginPackages: [
|
||||
{
|
||||
location: url.pathToFileURL(
|
||||
path.resolve(
|
||||
'/backstageRoot/dist-dynamic/test-backend-plugin/alpha',
|
||||
mockDir.resolve(
|
||||
'backstageRoot/dist-dynamic/test-backend-plugin/alpha',
|
||||
),
|
||||
),
|
||||
manifest: {
|
||||
@@ -502,32 +447,24 @@ Please add '${path.resolve(
|
||||
name: 'alpha manifest preferred but skipped because not a directory',
|
||||
preferAlpha: true,
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin': mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
}),
|
||||
alpha: mockFs.file({}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {
|
||||
'test-backend-plugin': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
alpha: '',
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expectedPluginPackages: [
|
||||
{
|
||||
location: url.pathToFileURL(
|
||||
path.resolve('/backstageRoot/dist-dynamic/test-backend-plugin'),
|
||||
mockDir.resolve('backstageRoot/dist-dynamic/test-backend-plugin'),
|
||||
),
|
||||
manifest: {
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
@@ -540,8 +477,8 @@ Please add '${path.resolve(
|
||||
expectedLogs: {
|
||||
warns: [
|
||||
{
|
||||
message: `skipping '${path.resolve(
|
||||
'/backstageRoot/dist-dynamic/test-backend-plugin/alpha',
|
||||
message: `skipping '${mockDir.resolve(
|
||||
'backstageRoot/dist-dynamic/test-backend-plugin/alpha',
|
||||
)}' since it is not a directory`,
|
||||
},
|
||||
],
|
||||
@@ -551,40 +488,28 @@ Please add '${path.resolve(
|
||||
name: 'invalid alpha package.json',
|
||||
preferAlpha: true,
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin': mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
}),
|
||||
alpha: mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: "invalid json content, 1, '",
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {
|
||||
'test-backend-plugin': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
alpha: {
|
||||
'package.json': "invalid json content, 1, '",
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expectedPluginPackages: [],
|
||||
expectedLogs: {
|
||||
errors: [
|
||||
{
|
||||
message: `failed to load dynamic plugin manifest from '${path.resolve(
|
||||
'/backstageRoot/dist-dynamic/test-backend-plugin/alpha',
|
||||
message: `failed to load dynamic plugin manifest from '${mockDir.resolve(
|
||||
'backstageRoot/dist-dynamic/test-backend-plugin/alpha',
|
||||
)}'`,
|
||||
meta: {
|
||||
name: 'SyntaxError',
|
||||
@@ -597,28 +522,20 @@ Please add '${path.resolve(
|
||||
{
|
||||
name: 'invalid package.json',
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin': mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: "invalid json content, 1, '",
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {
|
||||
'test-backend-plugin': {
|
||||
'package.json': "invalid json content, 1, '",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expectedPluginPackages: [],
|
||||
expectedLogs: {
|
||||
errors: [
|
||||
{
|
||||
message: `failed to load dynamic plugin manifest from '${path.resolve(
|
||||
'/backstageRoot/dist-dynamic/test-backend-plugin',
|
||||
message: `failed to load dynamic plugin manifest from '${mockDir.resolve(
|
||||
'backstageRoot/dist-dynamic/test-backend-plugin',
|
||||
)}'`,
|
||||
meta: {
|
||||
name: 'SyntaxError',
|
||||
@@ -631,32 +548,24 @@ Please add '${path.resolve(
|
||||
{
|
||||
name: 'missing backstage role in package.json',
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin': mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {
|
||||
'test-backend-plugin': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
main: 'dist/index.cjs.js',
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expectedPluginPackages: [],
|
||||
expectedLogs: {
|
||||
errors: [
|
||||
{
|
||||
message: `failed to load dynamic plugin manifest from '${path.resolve(
|
||||
'/backstageRoot/dist-dynamic/test-backend-plugin',
|
||||
message: `failed to load dynamic plugin manifest from '${mockDir.resolve(
|
||||
'backstageRoot/dist-dynamic/test-backend-plugin',
|
||||
)}'`,
|
||||
meta: {
|
||||
name: 'Error',
|
||||
@@ -669,32 +578,24 @@ Please add '${path.resolve(
|
||||
{
|
||||
name: 'missing main field in package.json',
|
||||
fileSystem: {
|
||||
'/backstageRoot': mockFs.directory({
|
||||
items: {
|
||||
'dist-dynamic': mockFs.directory({
|
||||
items: {
|
||||
'test-backend-plugin': mockFs.directory({
|
||||
items: {
|
||||
'package.json': mockFs.file({
|
||||
content: JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
backstageRoot: {
|
||||
'dist-dynamic': {
|
||||
'test-backend-plugin': {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-backend-plugin-dynamic',
|
||||
version: '0.0.0',
|
||||
backstage: { role: 'backend-plugin' },
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
expectedPluginPackages: [],
|
||||
expectedLogs: {
|
||||
errors: [
|
||||
{
|
||||
message: `failed to load dynamic plugin manifest from '${path.resolve(
|
||||
'/backstageRoot/dist-dynamic/test-backend-plugin',
|
||||
message: `failed to load dynamic plugin manifest from '${mockDir.resolve(
|
||||
'backstageRoot/dist-dynamic/test-backend-plugin',
|
||||
)}'`,
|
||||
meta: {
|
||||
name: 'Error',
|
||||
@@ -706,7 +607,7 @@ Please add '${path.resolve(
|
||||
},
|
||||
])('$name', async (tc: TestCase): Promise<void> => {
|
||||
const logger = new MockedLogger();
|
||||
const backstageRoot = '/backstageRoot';
|
||||
const backstageRoot = mockDir.resolve('backstageRoot');
|
||||
async function toTest(): Promise<ScannedPluginPackage[]> {
|
||||
const pluginScanner = new PluginScanner(
|
||||
new ConfigReader(
|
||||
@@ -725,7 +626,7 @@ Please add '${path.resolve(
|
||||
return await pluginScanner.scanRoot();
|
||||
}
|
||||
if (tc.fileSystem) {
|
||||
mockFs(tc.fileSystem);
|
||||
mockDir.setContent(tc.fileSystem);
|
||||
}
|
||||
if (tc.expectedError) {
|
||||
/* eslint-disable-next-line jest/no-conditional-expect */
|
||||
|
||||
@@ -54,9 +54,24 @@ export interface MockDirectory {
|
||||
|
||||
// @public
|
||||
export type MockDirectoryContent = {
|
||||
[name in string]: MockDirectoryContent | string | Buffer;
|
||||
[name in string]:
|
||||
| MockDirectoryContent
|
||||
| string
|
||||
| Buffer
|
||||
| MockDirectoryContentCallback;
|
||||
};
|
||||
|
||||
// @public
|
||||
export type MockDirectoryContentCallback = (
|
||||
ctx: MockDirectoryContentCallbackContext,
|
||||
) => void;
|
||||
|
||||
// @public
|
||||
export interface MockDirectoryContentCallbackContext {
|
||||
path: string;
|
||||
symlink(target: string): void;
|
||||
}
|
||||
|
||||
// @public
|
||||
export interface MockDirectoryContentOptions {
|
||||
path?: string;
|
||||
|
||||
@@ -91,7 +91,7 @@ describe('createMockDirectory', () => {
|
||||
|
||||
mockDir.addContent({
|
||||
'b.txt': 'b',
|
||||
b: {
|
||||
[mockDir.resolve('b')]: {
|
||||
'c.txt': 'c',
|
||||
},
|
||||
});
|
||||
@@ -119,6 +119,20 @@ describe('createMockDirectory', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to use callback for more detailed file system operations', () => {
|
||||
mockDir.setContent({
|
||||
'a.txt': 'a',
|
||||
'b.txt': ctx => ctx.symlink('./a.txt'),
|
||||
'c.txt': ctx => fs.copyFileSync(mockDir.resolve('a.txt'), ctx.path),
|
||||
});
|
||||
|
||||
expect(mockDir.content()).toEqual({
|
||||
'a.txt': 'a',
|
||||
'b.txt': 'a',
|
||||
'c.txt': 'a',
|
||||
});
|
||||
});
|
||||
|
||||
it('should read content from sub dirs', () => {
|
||||
mockDir.setContent({
|
||||
'a.txt': 'a',
|
||||
|
||||
@@ -30,6 +30,28 @@ import {
|
||||
|
||||
const tmpdirMarker = Symbol('os-tmpdir-mock');
|
||||
|
||||
/**
|
||||
* A context that allows for more advanced file system operations when writing mock directory content.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface MockDirectoryContentCallbackContext {
|
||||
/** Absolute path to the location of this piece of content on the filesystem */
|
||||
path: string;
|
||||
|
||||
/** Creates a symbolic link at the current location */
|
||||
symlink(target: string): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback that allows for more advanced file system operations when writing mock directory content.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type MockDirectoryContentCallback = (
|
||||
ctx: MockDirectoryContentCallbackContext,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* The content of a mock directory represented by a nested object structure.
|
||||
*
|
||||
@@ -54,7 +76,11 @@ const tmpdirMarker = Symbol('os-tmpdir-mock');
|
||||
* @public
|
||||
*/
|
||||
export type MockDirectoryContent = {
|
||||
[name in string]: MockDirectoryContent | string | Buffer;
|
||||
[name in string]:
|
||||
| MockDirectoryContent
|
||||
| string
|
||||
| Buffer
|
||||
| MockDirectoryContentCallback;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -178,6 +204,11 @@ type MockEntry =
|
||||
| {
|
||||
type: 'dir';
|
||||
path: string;
|
||||
}
|
||||
| {
|
||||
type: 'callback';
|
||||
path: string;
|
||||
callback: MockDirectoryContentCallback;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
@@ -214,10 +245,18 @@ class MockDirectoryImpl {
|
||||
}
|
||||
|
||||
if (entry.type === 'dir') {
|
||||
fs.ensureDirSync(fullPath, { mode: 0o777 });
|
||||
fs.ensureDirSync(fullPath);
|
||||
} else if (entry.type === 'file') {
|
||||
fs.ensureDirSync(dirname(fullPath), { mode: 0o777 });
|
||||
fs.writeFileSync(fullPath, entry.content, { mode: 0o666 });
|
||||
fs.ensureDirSync(dirname(fullPath));
|
||||
fs.writeFileSync(fullPath, entry.content);
|
||||
} else if (entry.type === 'callback') {
|
||||
fs.ensureDirSync(dirname(fullPath));
|
||||
entry.callback({
|
||||
path: fullPath,
|
||||
symlink(target: string) {
|
||||
fs.symlinkSync(target, fullPath);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,19 +318,20 @@ class MockDirectoryImpl {
|
||||
const entries: MockEntry[] = [];
|
||||
|
||||
function traverse(node: MockDirectoryContent[string], path: string) {
|
||||
const trimmedPath = path.startsWith('/') ? path.slice(1) : path; // trim leading slash
|
||||
if (typeof node === 'string') {
|
||||
entries.push({
|
||||
type: 'file',
|
||||
path: trimmedPath,
|
||||
path,
|
||||
content: Buffer.from(node, 'utf8'),
|
||||
});
|
||||
} else if (node instanceof Buffer) {
|
||||
entries.push({ type: 'file', path: trimmedPath, content: node });
|
||||
entries.push({ type: 'file', path, content: node });
|
||||
} else if (typeof node === 'function') {
|
||||
entries.push({ type: 'callback', path, callback: node });
|
||||
} else {
|
||||
entries.push({ type: 'dir', path: trimmedPath });
|
||||
entries.push({ type: 'dir', path });
|
||||
for (const [name, child] of Object.entries(node)) {
|
||||
traverse(child, `${trimmedPath}/${name}`);
|
||||
traverse(child, path ? `${path}/${name}` : name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,4 +20,6 @@ export {
|
||||
type MockDirectoryOptions,
|
||||
type MockDirectoryContent,
|
||||
type MockDirectoryContentOptions,
|
||||
type MockDirectoryContentCallback,
|
||||
type MockDirectoryContentCallbackContext,
|
||||
} from './MockDirectory';
|
||||
|
||||
@@ -57,7 +57,6 @@
|
||||
"@types/json-schema-merge-allof": "^0.6.0",
|
||||
"@types/mock-fs": "^4.10.0",
|
||||
"@types/yup": "^0.29.13",
|
||||
"mock-fs": "^5.2.0",
|
||||
"msw": "^1.0.0",
|
||||
"zen-observable": "^0.10.0"
|
||||
},
|
||||
|
||||
@@ -16,15 +16,62 @@
|
||||
|
||||
import { AppConfig } from '@backstage/config';
|
||||
import { loadConfig } from './loader';
|
||||
import mockFs from 'mock-fs';
|
||||
import fs from 'fs-extra';
|
||||
import { rest } from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { resolve as resolvePath, sep } from 'path';
|
||||
|
||||
const root = resolvePath('/');
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('loadConfig', () => {
|
||||
const mockDir = createMockDirectory({
|
||||
content: {
|
||||
'app-config.yaml': `
|
||||
app:
|
||||
title: Example App
|
||||
sessionKey:
|
||||
$file: secrets/session-key.txt
|
||||
escaped: \$\${Escaped}
|
||||
`,
|
||||
'app-config2.yaml': `
|
||||
app:
|
||||
title: Example App 2
|
||||
sessionKey:
|
||||
$file: secrets/session-key.txt
|
||||
escaped: \$\${Escaped}
|
||||
`,
|
||||
'app-config.development.yaml': `
|
||||
app:
|
||||
sessionKey: development-key
|
||||
backend:
|
||||
$include: ./included.yaml
|
||||
other:
|
||||
$include: secrets/included.yaml
|
||||
`,
|
||||
'secrets/session-key.txt': 'abc123',
|
||||
'secrets/included.yaml': `
|
||||
secret:
|
||||
$file: session-key.txt
|
||||
`,
|
||||
'included.yaml': `
|
||||
foo:
|
||||
bar: token \${MY_SECRET}
|
||||
`,
|
||||
'app-config.substitute.yaml': `
|
||||
app:
|
||||
someConfig:
|
||||
$include: \${SUBSTITUTE_ME}.yaml
|
||||
noSubstitute:
|
||||
$file: \$\${ESCAPE_ME}.txt
|
||||
`,
|
||||
'substituted.yaml': `
|
||||
secret:
|
||||
$file: secrets/\${SUBSTITUTE_ME}.txt
|
||||
`,
|
||||
'secrets/substituted.txt': '123abc',
|
||||
'${ESCAPE_ME}.txt': 'notSubstituted',
|
||||
'empty.yaml': '# just a comment',
|
||||
},
|
||||
});
|
||||
|
||||
const server = setupServer();
|
||||
const initialLoaderHandler = rest.get(
|
||||
`https://some.domain.io/app-config.yaml`,
|
||||
@@ -61,58 +108,9 @@ describe('loadConfig', () => {
|
||||
beforeEach(() => {
|
||||
process.env.MY_SECRET = 'is-secret';
|
||||
process.env.SUBSTITUTE_ME = 'substituted';
|
||||
|
||||
mockFs({
|
||||
'/root/app-config.yaml': `
|
||||
app:
|
||||
title: Example App
|
||||
sessionKey:
|
||||
$file: secrets/session-key.txt
|
||||
escaped: \$\${Escaped}
|
||||
`,
|
||||
'/root/app-config2.yaml': `
|
||||
app:
|
||||
title: Example App 2
|
||||
sessionKey:
|
||||
$file: secrets/session-key.txt
|
||||
escaped: \$\${Escaped}
|
||||
`,
|
||||
'/root/app-config.development.yaml': `
|
||||
app:
|
||||
sessionKey: development-key
|
||||
backend:
|
||||
$include: ./included.yaml
|
||||
other:
|
||||
$include: secrets/included.yaml
|
||||
`,
|
||||
'/root/secrets/session-key.txt': 'abc123',
|
||||
'/root/secrets/included.yaml': `
|
||||
secret:
|
||||
$file: session-key.txt
|
||||
`,
|
||||
'/root/included.yaml': `
|
||||
foo:
|
||||
bar: token \${MY_SECRET}
|
||||
`,
|
||||
'/root/app-config.substitute.yaml': `
|
||||
app:
|
||||
someConfig:
|
||||
$include: \${SUBSTITUTE_ME}.yaml
|
||||
noSubstitute:
|
||||
$file: \$\${ESCAPE_ME}.txt
|
||||
`,
|
||||
'/root/substituted.yaml': `
|
||||
secret:
|
||||
$file: secrets/\${SUBSTITUTE_ME}.txt
|
||||
`,
|
||||
'/root/secrets/substituted.txt': '123abc',
|
||||
'/root/${ESCAPE_ME}.txt': 'notSubstituted',
|
||||
'/root/empty.yaml': '# just a comment',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
server.resetHandlers();
|
||||
});
|
||||
|
||||
@@ -121,7 +119,7 @@ describe('loadConfig', () => {
|
||||
it('load config from default path', async () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configRoot: mockDir.path,
|
||||
configTargets: [],
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
@@ -135,7 +133,7 @@ describe('loadConfig', () => {
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
path: `${root}root${sep}app-config.yaml`,
|
||||
path: mockDir.resolve('app-config.yaml'),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -148,7 +146,7 @@ describe('loadConfig', () => {
|
||||
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configRoot: mockDir.path,
|
||||
configTargets: [{ url: configUrl }],
|
||||
remote: {
|
||||
reloadIntervalSeconds: 30,
|
||||
@@ -173,10 +171,10 @@ describe('loadConfig', () => {
|
||||
it('loads config with secrets from two different files', async () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configRoot: mockDir.path,
|
||||
configTargets: [
|
||||
{ path: '/root/app-config.yaml' },
|
||||
{ path: '/root/app-config2.yaml' },
|
||||
{ path: mockDir.resolve('app-config.yaml') },
|
||||
{ path: mockDir.resolve('app-config2.yaml') },
|
||||
],
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
@@ -190,7 +188,7 @@ describe('loadConfig', () => {
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
path: '/root/app-config.yaml',
|
||||
path: mockDir.resolve('app-config.yaml'),
|
||||
},
|
||||
{
|
||||
context: 'app-config2.yaml',
|
||||
@@ -201,7 +199,7 @@ describe('loadConfig', () => {
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
path: '/root/app-config2.yaml',
|
||||
path: mockDir.resolve('app-config2.yaml'),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -210,8 +208,8 @@ describe('loadConfig', () => {
|
||||
it('loads config with secrets from single file', async () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configTargets: [{ path: '/root/app-config.yaml' }],
|
||||
configRoot: mockDir.path,
|
||||
configTargets: [{ path: mockDir.resolve('app-config.yaml') }],
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
appConfigs: [
|
||||
@@ -224,7 +222,7 @@ describe('loadConfig', () => {
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
path: '/root/app-config.yaml',
|
||||
path: mockDir.resolve('app-config.yaml'),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -233,10 +231,10 @@ describe('loadConfig', () => {
|
||||
it('loads development config with secrets', async () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configRoot: mockDir.path,
|
||||
configTargets: [
|
||||
{ path: '/root/app-config.yaml' },
|
||||
{ path: '/root/app-config.development.yaml' },
|
||||
{ path: mockDir.resolve('app-config.yaml') },
|
||||
{ path: mockDir.resolve('app-config.development.yaml') },
|
||||
],
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
@@ -250,7 +248,7 @@ describe('loadConfig', () => {
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
path: '/root/app-config.yaml',
|
||||
path: mockDir.resolve('app-config.yaml'),
|
||||
},
|
||||
{
|
||||
context: 'app-config.development.yaml',
|
||||
@@ -267,7 +265,7 @@ describe('loadConfig', () => {
|
||||
secret: 'abc123',
|
||||
},
|
||||
},
|
||||
path: '/root/app-config.development.yaml',
|
||||
path: mockDir.resolve('app-config.development.yaml'),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -276,8 +274,10 @@ describe('loadConfig', () => {
|
||||
it('loads deep substituted config', async () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configTargets: [{ path: '/root/app-config.substitute.yaml' }],
|
||||
configRoot: mockDir.path,
|
||||
configTargets: [
|
||||
{ path: mockDir.resolve('app-config.substitute.yaml') },
|
||||
],
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
appConfigs: [
|
||||
@@ -291,7 +291,7 @@ describe('loadConfig', () => {
|
||||
noSubstitute: 'notSubstituted',
|
||||
},
|
||||
},
|
||||
path: '/root/app-config.substitute.yaml',
|
||||
path: mockDir.resolve('app-config.substitute.yaml'),
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -303,7 +303,7 @@ describe('loadConfig', () => {
|
||||
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configRoot: mockDir.path,
|
||||
configTargets: [],
|
||||
watch: {
|
||||
onChange: onChange.resolve,
|
||||
@@ -321,12 +321,12 @@ describe('loadConfig', () => {
|
||||
escaped: '${Escaped}',
|
||||
},
|
||||
},
|
||||
path: `${root}root${sep}app-config.yaml`,
|
||||
path: mockDir.resolve('app-config.yaml'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await fs.writeJson('/root/app-config.yaml', {
|
||||
await fs.writeJson(mockDir.resolve('app-config.yaml'), {
|
||||
app: {
|
||||
title: 'New Title',
|
||||
},
|
||||
@@ -339,7 +339,7 @@ describe('loadConfig', () => {
|
||||
title: 'New Title',
|
||||
},
|
||||
},
|
||||
path: `${root}root${sep}app-config.yaml`,
|
||||
path: mockDir.resolve('app-config.yaml'),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -352,8 +352,10 @@ describe('loadConfig', () => {
|
||||
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configTargets: [{ path: '/root/app-config.development.yaml' }],
|
||||
configRoot: mockDir.path,
|
||||
configTargets: [
|
||||
{ path: mockDir.resolve('app-config.development.yaml') },
|
||||
],
|
||||
watch: {
|
||||
onChange: onChange.resolve,
|
||||
stopSignal: stopSignal.promise,
|
||||
@@ -376,14 +378,14 @@ describe('loadConfig', () => {
|
||||
secret: 'abc123',
|
||||
},
|
||||
},
|
||||
path: '/root/app-config.development.yaml',
|
||||
path: mockDir.resolve('app-config.development.yaml'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// session-key is indirectly included in app-config.development.yaml
|
||||
// via included.yaml
|
||||
await fs.writeFile('/root/secrets/session-key.txt', 'abc234');
|
||||
await fs.writeFile(mockDir.resolve('secrets/session-key.txt'), 'abc234');
|
||||
|
||||
await expect(onChange.promise).resolves.toEqual([
|
||||
{
|
||||
@@ -401,7 +403,7 @@ describe('loadConfig', () => {
|
||||
secret: 'abc234',
|
||||
},
|
||||
},
|
||||
path: '/root/app-config.development.yaml',
|
||||
path: mockDir.resolve('app-config.development.yaml'),
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -417,7 +419,7 @@ describe('loadConfig', () => {
|
||||
const configUrl = 'https://some.domain.io/app-config.yaml';
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configRoot: mockDir.path,
|
||||
configTargets: [{ url: configUrl }],
|
||||
watch: {
|
||||
onChange: onChange.resolve,
|
||||
@@ -464,7 +466,7 @@ describe('loadConfig', () => {
|
||||
const stopSignal = defer<void>();
|
||||
|
||||
await loadConfig({
|
||||
configRoot: '/root',
|
||||
configRoot: mockDir.path,
|
||||
configTargets: [],
|
||||
watch: {
|
||||
onChange: () => {
|
||||
@@ -476,7 +478,7 @@ describe('loadConfig', () => {
|
||||
|
||||
stopSignal.resolve();
|
||||
|
||||
await fs.writeJson('/root/app-config.yaml', {
|
||||
await fs.writeJson(mockDir.resolve('app-config.yaml'), {
|
||||
app: {
|
||||
title: 'New Title',
|
||||
},
|
||||
@@ -487,8 +489,8 @@ describe('loadConfig', () => {
|
||||
it('handles empty files gracefully', async () => {
|
||||
await expect(
|
||||
loadConfig({
|
||||
configRoot: '/root',
|
||||
configTargets: [{ path: '/root/empty.yaml' }],
|
||||
configRoot: mockDir.path,
|
||||
configTargets: [{ path: mockDir.resolve('empty.yaml') }],
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
appConfigs: [],
|
||||
|
||||
@@ -14,10 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import mockFs from 'mock-fs';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { collectConfigSchemas } from './collect';
|
||||
import path from 'path';
|
||||
|
||||
// cwd must be restored
|
||||
const origDir = process.cwd();
|
||||
afterAll(() => {
|
||||
process.chdir(origDir);
|
||||
});
|
||||
|
||||
const mockSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -28,25 +34,15 @@ const mockSchema = {
|
||||
},
|
||||
};
|
||||
|
||||
// Gotta make sure this is in the compiler cache before we start mocking the filesystem
|
||||
require('typescript-json-schema');
|
||||
|
||||
// We need to load in actual TS libraries when using mock-fs.
|
||||
// This lookup is to allow the `typescript` dependency to exist either
|
||||
// at top level or inside node_modules of typescript-json-schema
|
||||
const typescriptModuleDir = path.dirname(
|
||||
require.resolve('typescript/package.json', {
|
||||
paths: [require.resolve('typescript-json-schema')],
|
||||
}),
|
||||
);
|
||||
|
||||
describe('collectConfigSchemas', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
mockDir.clear();
|
||||
});
|
||||
|
||||
it('should not find any schemas without packages', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
'lerna.json': JSON.stringify({
|
||||
packages: ['packages/*'],
|
||||
}),
|
||||
@@ -56,7 +52,7 @@ describe('collectConfigSchemas', () => {
|
||||
});
|
||||
|
||||
it('should find schema in a local package', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
node_modules: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
@@ -66,6 +62,7 @@ describe('collectConfigSchemas', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
process.chdir(mockDir.path);
|
||||
|
||||
await expect(collectConfigSchemas(['a'], [])).resolves.toEqual([
|
||||
{
|
||||
@@ -76,7 +73,7 @@ describe('collectConfigSchemas', () => {
|
||||
});
|
||||
|
||||
it('should find schema at explicit package path', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
root: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'root',
|
||||
@@ -84,6 +81,7 @@ describe('collectConfigSchemas', () => {
|
||||
}),
|
||||
},
|
||||
});
|
||||
process.chdir(mockDir.path);
|
||||
|
||||
await expect(
|
||||
collectConfigSchemas([], [path.join('root', 'package.json')]),
|
||||
@@ -96,7 +94,7 @@ describe('collectConfigSchemas', () => {
|
||||
});
|
||||
|
||||
it('should find schema in transitive dependencies and explicit path', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
root: {
|
||||
'package.json': JSON.stringify({
|
||||
name: 'root',
|
||||
@@ -152,6 +150,7 @@ describe('collectConfigSchemas', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
process.chdir(mockDir.path);
|
||||
|
||||
await expect(
|
||||
collectConfigSchemas(['a'], [path.join('root', 'package.json')]),
|
||||
@@ -178,7 +177,7 @@ describe('collectConfigSchemas', () => {
|
||||
});
|
||||
|
||||
it('should schema of different types', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
node_modules: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
@@ -198,15 +197,16 @@ describe('collectConfigSchemas', () => {
|
||||
name: 'c',
|
||||
configSchema: 'schema.d.ts',
|
||||
}),
|
||||
'schema.d.ts': `export interface Config {
|
||||
'schema.d.ts': `
|
||||
export interface Config {
|
||||
/** @visibility secret */
|
||||
tsKey: string
|
||||
}`,
|
||||
}
|
||||
`,
|
||||
},
|
||||
},
|
||||
// TypeScript compilation needs to load some real files inside the typescript dir
|
||||
[typescriptModuleDir]: (mockFs as any).load(typescriptModuleDir),
|
||||
});
|
||||
process.chdir(mockDir.path);
|
||||
|
||||
await expect(collectConfigSchemas(['a', 'b', 'c'], [])).resolves.toEqual([
|
||||
{
|
||||
@@ -235,7 +235,7 @@ describe('collectConfigSchemas', () => {
|
||||
});
|
||||
|
||||
it('should load schema from different package versions', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
node_modules: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
@@ -275,6 +275,7 @@ describe('collectConfigSchemas', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
process.chdir(mockDir.path);
|
||||
|
||||
await expect(collectConfigSchemas(['a'], [])).resolves.toEqual([
|
||||
{
|
||||
@@ -303,7 +304,7 @@ describe('collectConfigSchemas', () => {
|
||||
});
|
||||
|
||||
it('should not allow unknown schema file types', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
node_modules: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
@@ -314,6 +315,7 @@ describe('collectConfigSchemas', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
process.chdir(mockDir.path);
|
||||
|
||||
await expect(collectConfigSchemas(['a'], [])).rejects.toThrow(
|
||||
'Config schema files must be .json or .d.ts, got schema.yaml',
|
||||
@@ -321,7 +323,7 @@ describe('collectConfigSchemas', () => {
|
||||
});
|
||||
|
||||
it('should reject typescript config declaration without a Config type', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
node_modules: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
@@ -331,9 +333,8 @@ describe('collectConfigSchemas', () => {
|
||||
'schema.d.ts': `export interface NotConfig {}`,
|
||||
},
|
||||
},
|
||||
// TypeScript compilation needs to load some real files inside the typescript dir
|
||||
[typescriptModuleDir]: (mockFs as any).load(typescriptModuleDir),
|
||||
});
|
||||
process.chdir(mockDir.path);
|
||||
|
||||
await expect(collectConfigSchemas(['a'], [])).rejects.toThrow(
|
||||
`Invalid schema in ${path.join(
|
||||
|
||||
@@ -14,16 +14,24 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import mockFs from 'mock-fs';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { loadConfigSchema } from './load';
|
||||
|
||||
// cwd must be restored
|
||||
const origDir = process.cwd();
|
||||
afterAll(() => {
|
||||
process.chdir(origDir);
|
||||
});
|
||||
|
||||
describe('loadConfigSchema', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
mockDir.clear();
|
||||
});
|
||||
|
||||
it('should load schema from packages or data', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
node_modules: {
|
||||
a: {
|
||||
'package.json': JSON.stringify({
|
||||
@@ -53,6 +61,7 @@ describe('loadConfigSchema', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
process.chdir(mockDir.path);
|
||||
|
||||
const schema = await loadConfigSchema({
|
||||
dependencies: ['a'],
|
||||
@@ -119,7 +128,7 @@ describe('loadConfigSchema', () => {
|
||||
|
||||
describe('should consider schema', () => {
|
||||
it('when filtering simple config', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
configSchema: {
|
||||
@@ -131,6 +140,7 @@ describe('loadConfigSchema', () => {
|
||||
},
|
||||
}),
|
||||
});
|
||||
process.chdir(mockDir.path);
|
||||
|
||||
const schema = await loadConfigSchema({
|
||||
packagePaths: ['package.json'],
|
||||
@@ -156,7 +166,7 @@ describe('loadConfigSchema', () => {
|
||||
});
|
||||
|
||||
it('when filtering nested config', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
configSchema: {
|
||||
@@ -185,6 +195,7 @@ describe('loadConfigSchema', () => {
|
||||
},
|
||||
}),
|
||||
});
|
||||
process.chdir(mockDir.path);
|
||||
|
||||
const schema = await loadConfigSchema({
|
||||
packagePaths: ['package.json'],
|
||||
@@ -244,7 +255,7 @@ describe('loadConfigSchema', () => {
|
||||
});
|
||||
|
||||
it('when filtering config with required values', async () => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
'package.json': JSON.stringify({
|
||||
name: 'a',
|
||||
configSchema: {
|
||||
@@ -261,6 +272,7 @@ describe('loadConfigSchema', () => {
|
||||
},
|
||||
}),
|
||||
});
|
||||
process.chdir(mockDir.path);
|
||||
|
||||
const schema = await loadConfigSchema({
|
||||
packagePaths: ['package.json'],
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"recursive-readdir": "^2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/command-exists": "^1.2.0",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
*/
|
||||
|
||||
import inquirer from 'inquirer';
|
||||
import mockFs from 'mock-fs';
|
||||
import path from 'path';
|
||||
import { Command } from 'commander';
|
||||
import * as tasks from './lib/tasks';
|
||||
import createApp from './createApp';
|
||||
import { findPaths } from '@backstage/cli-common';
|
||||
import { tmpdir } from 'os';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
jest.mock('./lib/tasks');
|
||||
|
||||
@@ -40,16 +40,7 @@ const moveAppMock = jest.spyOn(tasks, 'moveAppTask');
|
||||
const buildAppMock = jest.spyOn(tasks, 'buildAppTask');
|
||||
|
||||
describe('command entrypoint', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[`${__dirname}/package.json`]: '', // required by `findPaths(__dirname)`
|
||||
'templates/': mockFs.load(path.resolve(__dirname, '../templates/')),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
const mockDir = createMockDirectory({ mockOsTmpDir: true });
|
||||
|
||||
beforeEach(() => {
|
||||
promptMock.mockResolvedValueOnce({
|
||||
@@ -62,6 +53,7 @@ describe('command entrypoint', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockDir.clear();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
@@ -75,7 +67,6 @@ describe('command entrypoint', () => {
|
||||
findPaths(__dirname).resolveTarget(
|
||||
'packages',
|
||||
'create-app',
|
||||
'src',
|
||||
'templates',
|
||||
'default-app',
|
||||
),
|
||||
@@ -97,7 +88,6 @@ describe('command entrypoint', () => {
|
||||
findPaths(__dirname).resolveTarget(
|
||||
'packages',
|
||||
'create-app',
|
||||
'src',
|
||||
'templates',
|
||||
'default-app',
|
||||
),
|
||||
|
||||
@@ -15,9 +15,8 @@
|
||||
*/
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import child_process from 'child_process';
|
||||
import path, { resolve as resolvePath } from 'path';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import os from 'os';
|
||||
import {
|
||||
Task,
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
tryInitGitRepository,
|
||||
readGitConfig,
|
||||
} from './tasks';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
jest.spyOn(Task, 'log').mockReturnValue(undefined);
|
||||
jest.spyOn(Task, 'error').mockReturnValue(undefined);
|
||||
@@ -101,21 +101,33 @@ describe('tasks', () => {
|
||||
) => void
|
||||
>;
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
const realChdir = process.chdir;
|
||||
// If anyone calls chdir then make it resolve within the tmpdir
|
||||
const mockChdir = jest.spyOn(process, 'chdir');
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'projects/my-module.ts': '',
|
||||
'projects/dir/my-file.txt': '',
|
||||
'tmp/mockApp/.gitignore': '',
|
||||
'tmp/mockApp/package.json': '',
|
||||
'tmp/mockApp/packages/app/package.json': '',
|
||||
// load templates into mock filesystem
|
||||
'templates/': mockFs.load(path.resolve(__dirname, '../../templates/')),
|
||||
mockDir.setContent({
|
||||
projects: {
|
||||
'my-module.ts': '',
|
||||
'dir/my-file.txt': '',
|
||||
},
|
||||
'tmp/mockApp': {
|
||||
'.gitignore': '',
|
||||
'package.json': '',
|
||||
'packages/app/package.json': '',
|
||||
},
|
||||
});
|
||||
realChdir(mockDir.path);
|
||||
mockChdir.mockImplementation((dir: string) =>
|
||||
realChdir(mockDir.resolve(dir)),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockExec.mockRestore();
|
||||
mockFs.restore();
|
||||
mockChdir.mockReset();
|
||||
});
|
||||
|
||||
describe('checkAppExistsTask', () => {
|
||||
@@ -164,8 +176,6 @@ describe('tasks', () => {
|
||||
|
||||
describe('buildAppTask', () => {
|
||||
it('should change to `appDir` and run `yarn install` and `yarn tsc`', async () => {
|
||||
const mockChdir = jest.spyOn(process, 'chdir');
|
||||
|
||||
// requires callback implementation to support `promisify` wrapper
|
||||
// https://stackoverflow.com/a/60579617/10044859
|
||||
mockExec.mockImplementation((_command, callback) => {
|
||||
@@ -199,8 +209,6 @@ describe('tasks', () => {
|
||||
});
|
||||
|
||||
it('should error out on incorrect yarn version', async () => {
|
||||
const mockChdir = jest.spyOn(process, 'chdir');
|
||||
|
||||
// requires callback implementation to support `promisify` wrapper
|
||||
// https://stackoverflow.com/a/60579617/10044859
|
||||
mockExec.mockImplementation((_command, callback) => {
|
||||
@@ -265,7 +273,7 @@ describe('tasks', () => {
|
||||
|
||||
describe('templatingTask', () => {
|
||||
it('should generate a project populating context parameters', async () => {
|
||||
const templateDir = 'templates/default-app';
|
||||
const templateDir = resolvePath(__dirname, '../../templates/default-app');
|
||||
const destinationDir = 'templatedApp';
|
||||
const context = {
|
||||
name: 'SuperCoolBackstageInstance',
|
||||
|
||||
@@ -427,6 +427,8 @@ async function dropClientDatabases(client: string) {
|
||||
async function testBackendStart(appDir: string, ...args: string[]) {
|
||||
const child = spawnPiped(['yarn', 'workspace', 'backend', 'start', ...args], {
|
||||
cwd: appDir,
|
||||
// Windows does not like piping stdin here, the child process will hang when requiring the 'process' module
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
env: {
|
||||
...process.env,
|
||||
GITHUB_TOKEN: 'abc',
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
|
||||
export const Core = createExtension({
|
||||
id: 'core',
|
||||
at: 'root',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
inputs: {
|
||||
apis: createExtensionInput({
|
||||
api: coreExtensionData.apiFactory,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { SidebarPage } from '@backstage/core-components';
|
||||
|
||||
export const CoreLayout = createExtension({
|
||||
id: 'core.layout',
|
||||
at: 'root',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
inputs: {
|
||||
nav: createExtensionInput(
|
||||
{
|
||||
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
createExtension,
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
useRouteRef,
|
||||
NavTarget,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { useRouteRef } from '@backstage/core-plugin-api';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -66,14 +66,14 @@ const SidebarLogo = () => {
|
||||
|
||||
const SidebarNavItem = (props: NavTarget) => {
|
||||
const { icon: Icon, title, routeRef } = props;
|
||||
const to = useRouteRef(routeRef)();
|
||||
const to = useRouteRef(routeRef)({});
|
||||
// TODO: Support opening modal, for example, the search one
|
||||
return <SidebarItem to={to} icon={Icon} text={title} />;
|
||||
};
|
||||
|
||||
export const CoreNav = createExtension({
|
||||
id: 'core.nav',
|
||||
at: 'core.layout/nav',
|
||||
attachTo: { id: 'core.layout', input: 'nav' },
|
||||
inputs: {
|
||||
items: createExtensionInput({
|
||||
target: coreExtensionData.navTarget,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useRoutes } from 'react-router-dom';
|
||||
|
||||
export const CoreRoutes = createExtension({
|
||||
id: 'core.routes',
|
||||
at: 'core.layout/content',
|
||||
attachTo: { id: 'core.layout', input: 'content' },
|
||||
inputs: {
|
||||
routes: createExtensionInput({
|
||||
path: coreExtensionData.routePath,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 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/core-plugin-api';
|
||||
import React, { createContext, ReactNode } from 'react';
|
||||
|
||||
export interface RoutingContextType {
|
||||
resolve(
|
||||
routeRef: RouteRef,
|
||||
options: { pathname: string },
|
||||
): (() => string) | undefined;
|
||||
}
|
||||
|
||||
export const RoutingContext = createContext<RoutingContextType>({
|
||||
resolve: () => () => '',
|
||||
});
|
||||
|
||||
export class RouteResolver {
|
||||
constructor(private readonly routePaths: Map<RouteRef, string>) {}
|
||||
|
||||
resolve(anyRouteRef: RouteRef<{}>): (() => string) | undefined {
|
||||
const basePath = this.routePaths.get(anyRouteRef);
|
||||
if (!basePath) {
|
||||
return undefined;
|
||||
}
|
||||
return () => basePath;
|
||||
}
|
||||
}
|
||||
|
||||
export function RoutingProvider(props: {
|
||||
routePaths: Map<RouteRef, string>;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<RoutingContext.Provider value={new RouteResolver(props.routePaths)}>
|
||||
{props.children}
|
||||
</RoutingContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -40,13 +40,15 @@ const refOrder = [ref1, ref2, ref3, ref4, ref5];
|
||||
|
||||
function createTestExtension(options: {
|
||||
id: string;
|
||||
at?: string;
|
||||
parent?: string;
|
||||
path?: string;
|
||||
routeRef?: RouteRef;
|
||||
}) {
|
||||
return createExtension({
|
||||
id: options.id,
|
||||
at: options.at ?? 'core.routes/children',
|
||||
attachTo: options.parent
|
||||
? { id: options.parent, input: 'children' }
|
||||
: { id: 'core.routes', input: 'children' },
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
path: coreExtensionData.routePath.optional(),
|
||||
@@ -126,13 +128,13 @@ describe('discovery', () => {
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'page1/children',
|
||||
parent: 'page1',
|
||||
path: 'bar/:id',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
at: 'page2/children',
|
||||
parent: 'page2',
|
||||
path: 'baz',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
@@ -143,7 +145,7 @@ describe('discovery', () => {
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
at: 'page1/children',
|
||||
parent: 'page1',
|
||||
path: 'blop',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
@@ -194,7 +196,7 @@ describe('discovery', () => {
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'page1/children',
|
||||
parent: 'page1',
|
||||
path: 'bar/:id',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
@@ -205,13 +207,13 @@ describe('discovery', () => {
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
at: 'page3/children',
|
||||
parent: 'page3',
|
||||
path: 'divsoup',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
at: 'page3/children',
|
||||
parent: 'page3',
|
||||
path: 'blop',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
@@ -242,7 +244,7 @@ describe('discovery', () => {
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'page1/children',
|
||||
parent: 'page1',
|
||||
path: '/bar/:id',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
@@ -253,13 +255,13 @@ describe('discovery', () => {
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
at: 'page3/children',
|
||||
parent: 'page3',
|
||||
path: '/divsoup',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
at: 'page3/children',
|
||||
parent: 'page3',
|
||||
path: '/blop',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
@@ -289,16 +291,16 @@ describe('discovery', () => {
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
at: 'foo/children',
|
||||
parent: 'foo',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'fooChild',
|
||||
at: 'foo/children',
|
||||
parent: 'foo',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'fooChild/children',
|
||||
parent: 'fooChild',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
@@ -311,17 +313,17 @@ describe('discovery', () => {
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3Child',
|
||||
at: 'page3/children',
|
||||
parent: 'page3',
|
||||
path: '',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
at: 'page3Child/children',
|
||||
parent: 'page3Child',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
at: 'page4/children',
|
||||
parent: 'page4',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
]);
|
||||
@@ -361,29 +363,29 @@ describe('discovery', () => {
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page1Child',
|
||||
at: 'page1/children',
|
||||
parent: 'page1',
|
||||
path: 'bar',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'page1Child/children',
|
||||
parent: 'page1Child',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
at: 'page2/children',
|
||||
parent: 'page2',
|
||||
path: 'baz',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
at: 'page3/children',
|
||||
parent: 'page3',
|
||||
path: '/blop',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
at: 'page2/children',
|
||||
parent: 'page2',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
]);
|
||||
@@ -445,30 +447,30 @@ describe('discovery', () => {
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
at: 'r/children',
|
||||
parent: 'r',
|
||||
path: 'x',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'y',
|
||||
path: 'y',
|
||||
at: 'r/children',
|
||||
parent: 'r',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
at: 'y/children',
|
||||
parent: 'y',
|
||||
path: '1',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
at: 'page2/children',
|
||||
parent: 'page2',
|
||||
path: 'a',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
at: 'page2/children',
|
||||
parent: 'page2',
|
||||
path: 'b',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
|
||||
@@ -25,6 +25,7 @@ import { screen } from '@testing-library/react';
|
||||
import { MockConfigApi, renderWithEffects } from '@backstage/test-utils';
|
||||
import React from 'react';
|
||||
import { createRouteRef } from '@backstage/core-plugin-api';
|
||||
import { createExtensionInstance } from './createExtensionInstance';
|
||||
|
||||
describe('createInstances', () => {
|
||||
it('throws an error when a root extension is parametrized', () => {
|
||||
@@ -32,9 +33,7 @@ describe('createInstances', () => {
|
||||
app: {
|
||||
extensions: [
|
||||
{
|
||||
root: {
|
||||
at: '',
|
||||
},
|
||||
root: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -58,7 +57,7 @@ describe('createInstances', () => {
|
||||
extensions: [
|
||||
createExtension({
|
||||
id: 'root',
|
||||
at: 'core.routes/route',
|
||||
attachTo: { id: 'core.routes', input: 'route' },
|
||||
inputs: {},
|
||||
output: {},
|
||||
factory() {},
|
||||
@@ -134,4 +133,112 @@ describe('createApp', () => {
|
||||
|
||||
await expect(screen.findByText('Derp')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should log an app', () => {
|
||||
const { rootInstances } = createInstances({
|
||||
config: new MockConfigApi({}),
|
||||
plugins: [],
|
||||
});
|
||||
const root = createExtensionInstance({
|
||||
extension: createExtension({
|
||||
id: 'root',
|
||||
attachTo: { id: '', input: '' },
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
config: undefined,
|
||||
attachments: new Map([['children', rootInstances]]),
|
||||
});
|
||||
|
||||
expect(String(root)).toMatchInlineSnapshot(`
|
||||
"<root>
|
||||
children [
|
||||
<core>
|
||||
themes [
|
||||
<themes.light out=[core.theme] />
|
||||
<themes.dark out=[core.theme] />
|
||||
]
|
||||
</core>
|
||||
<core.layout out=[core.reactElement]>
|
||||
content [
|
||||
<core.routes out=[core.reactElement] />
|
||||
]
|
||||
nav [
|
||||
<core.nav out=[core.reactElement] />
|
||||
]
|
||||
</core.layout>
|
||||
]
|
||||
</root>"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should serialize an app as JSON', () => {
|
||||
const { rootInstances } = createInstances({
|
||||
config: new MockConfigApi({}),
|
||||
plugins: [],
|
||||
});
|
||||
const root = createExtensionInstance({
|
||||
extension: createExtension({
|
||||
id: 'root',
|
||||
attachTo: { id: '', input: '' },
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
config: undefined,
|
||||
attachments: new Map([['children', rootInstances]]),
|
||||
});
|
||||
|
||||
expect(JSON.parse(JSON.stringify(root))).toMatchInlineSnapshot(`
|
||||
{
|
||||
"attachments": {
|
||||
"children": [
|
||||
{
|
||||
"attachments": {
|
||||
"themes": [
|
||||
{
|
||||
"id": "themes.light",
|
||||
"output": [
|
||||
"core.theme",
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "themes.dark",
|
||||
"output": [
|
||||
"core.theme",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"id": "core",
|
||||
},
|
||||
{
|
||||
"attachments": {
|
||||
"content": [
|
||||
{
|
||||
"id": "core.routes",
|
||||
"output": [
|
||||
"core.reactElement",
|
||||
],
|
||||
},
|
||||
],
|
||||
"nav": [
|
||||
{
|
||||
"id": "core.nav",
|
||||
"output": [
|
||||
"core.reactElement",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"id": "core.layout",
|
||||
"output": [
|
||||
"core.reactElement",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
"id": "root",
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
mergeExtensionParameters,
|
||||
readAppExtensionParameters,
|
||||
} from './parameters';
|
||||
import { RoutingProvider } from '../routing/RoutingContext';
|
||||
import {
|
||||
AnyApiFactory,
|
||||
ApiHolder,
|
||||
@@ -74,7 +73,7 @@ import { defaultConfigLoaderSync } from '../../../core-app-api/src/app/defaultCo
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { overrideBaseUrlConfigs } from '../../../core-app-api/src/app/overrideBaseUrlConfigs';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { RoutingProvider as LegacyRoutingProvider } from '../../../core-app-api/src/routing/RoutingProvider';
|
||||
import { RoutingProvider } from '../../../core-app-api/src/routing/RoutingProvider';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
apis as defaultApis,
|
||||
@@ -202,8 +201,8 @@ export function createInstances(options: {
|
||||
Map<string, ExtensionInstanceParameters[]>
|
||||
>();
|
||||
for (const instanceParams of extensionParams) {
|
||||
const [extensionId, pointId = 'default'] = instanceParams.at.split('/');
|
||||
|
||||
const extensionId = instanceParams.attachTo.id;
|
||||
const pointId = instanceParams.attachTo.input;
|
||||
let pointMap = attachmentMap.get(extensionId);
|
||||
if (!pointMap) {
|
||||
pointMap = new Map();
|
||||
@@ -308,15 +307,10 @@ export function createApp(options: {
|
||||
<ApiProvider apis={apiHolder}>
|
||||
<AppContextProvider appContext={appContext}>
|
||||
<AppThemeProvider>
|
||||
<LegacyRoutingProvider
|
||||
{...routeInfo}
|
||||
routeBindings={new Map(/* TODO */)}
|
||||
>
|
||||
<RoutingProvider routePaths={routeInfo.routePaths}>
|
||||
{/* TODO: set base path using the logic from AppRouter */}
|
||||
<BrowserRouter>{rootElements}</BrowserRouter>
|
||||
</RoutingProvider>
|
||||
</LegacyRoutingProvider>
|
||||
<RoutingProvider {...routeInfo} routeBindings={new Map(/* TODO */)}>
|
||||
{/* TODO: set base path using the logic from AppRouter */}
|
||||
<BrowserRouter>{rootElements}</BrowserRouter>
|
||||
</RoutingProvider>
|
||||
</AppThemeProvider>
|
||||
</AppContextProvider>
|
||||
</ApiProvider>
|
||||
|
||||
@@ -28,7 +28,7 @@ const inputMirrorDataRef = createExtensionDataRef<unknown>('mirror');
|
||||
|
||||
const simpleExtension = createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
@@ -101,7 +101,7 @@ describe('createExtensionInstance', () => {
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
optionalSingletonPresent: createExtensionInput(
|
||||
{
|
||||
@@ -166,7 +166,7 @@ describe('createExtensionInstance', () => {
|
||||
config: { other: 'not-a-number' },
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {},
|
||||
factory() {
|
||||
const error = new Error('NOPE');
|
||||
@@ -188,7 +188,7 @@ describe('createExtensionInstance', () => {
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test1: testDataRef,
|
||||
test2: testDataRef,
|
||||
@@ -211,7 +211,7 @@ describe('createExtensionInstance', () => {
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
},
|
||||
@@ -232,7 +232,7 @@ describe('createExtensionInstance', () => {
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
@@ -273,7 +273,7 @@ describe('createExtensionInstance', () => {
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
@@ -314,7 +314,7 @@ describe('createExtensionInstance', () => {
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
@@ -350,7 +350,7 @@ describe('createExtensionInstance', () => {
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
|
||||
@@ -90,6 +90,68 @@ function resolveInputs(
|
||||
});
|
||||
}
|
||||
|
||||
function indent(str: string) {
|
||||
return str.replace(/^/gm, ' ');
|
||||
}
|
||||
|
||||
class ExtensionInstanceImpl implements ExtensionInstance {
|
||||
readonly $$type = '@backstage/ExtensionInstance';
|
||||
|
||||
readonly id: string;
|
||||
readonly #extensionData: Map<string, unknown>;
|
||||
readonly attachments: Map<string, ExtensionInstance[]>;
|
||||
readonly source?: BackstagePlugin;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
extensionData: Map<string, unknown>,
|
||||
attachments: Map<string, ExtensionInstance[]>,
|
||||
source: BackstagePlugin | undefined,
|
||||
) {
|
||||
this.id = id;
|
||||
this.#extensionData = extensionData;
|
||||
this.attachments = attachments;
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
getData<T>(ref: ExtensionDataRef<T>): T | undefined {
|
||||
return this.#extensionData.get(ref.id) as T | undefined;
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
output:
|
||||
this.#extensionData.size > 0
|
||||
? [...this.#extensionData.keys()]
|
||||
: undefined,
|
||||
attachments:
|
||||
this.attachments.size > 0
|
||||
? Object.fromEntries(this.attachments)
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
toString() {
|
||||
const out =
|
||||
this.#extensionData.size > 0
|
||||
? ` out=[${[...this.#extensionData.keys()].join(', ')}]`
|
||||
: '';
|
||||
|
||||
if (this.attachments.size === 0) {
|
||||
return `<${this.id}${out} />`;
|
||||
}
|
||||
|
||||
return [
|
||||
`<${this.id}${out}>`,
|
||||
...[...this.attachments.entries()].map(([k, v]) =>
|
||||
indent([`${k} [`, ...v.map(e => indent(e.toString())), `]`].join('\n')),
|
||||
),
|
||||
`</${this.id}>`,
|
||||
].join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createExtensionInstance(options: {
|
||||
extension: Extension<unknown>;
|
||||
@@ -137,13 +199,10 @@ export function createExtensionInstance(options: {
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
$$type: '@backstage/ExtensionInstance',
|
||||
id: options.extension.id,
|
||||
getData<T>(ref: ExtensionDataRef<T>): T | undefined {
|
||||
return extensionData.get(ref.id) as T | undefined;
|
||||
},
|
||||
source,
|
||||
return new ExtensionInstanceImpl(
|
||||
options.extension.id,
|
||||
extensionData,
|
||||
attachments,
|
||||
};
|
||||
source,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
function makeExt(id: string, status: 'disabled' | 'enabled' = 'enabled') {
|
||||
return {
|
||||
id,
|
||||
at: 'root',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
disabled: status === 'disabled',
|
||||
} as Extension<unknown>;
|
||||
}
|
||||
@@ -52,8 +52,8 @@ describe('mergeExtensionParameters', () => {
|
||||
parameters: [],
|
||||
}),
|
||||
).toEqual([
|
||||
{ extension: a, at: 'root' },
|
||||
{ extension: b, at: 'root' },
|
||||
{ extension: a, attachTo: { id: 'root', input: 'default' } },
|
||||
{ extension: b, attachTo: { id: 'root', input: 'default' } },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -68,13 +68,17 @@ describe('mergeExtensionParameters', () => {
|
||||
parameters: [
|
||||
{
|
||||
id: 'b',
|
||||
at: 'derp',
|
||||
attachTo: { id: 'derp', input: 'default' },
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
{ extension: a, at: 'root', source: pluginA },
|
||||
{ extension: b, at: 'derp' },
|
||||
{
|
||||
extension: a,
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
source: pluginA,
|
||||
},
|
||||
{ extension: b, attachTo: { id: 'derp', input: 'default' } },
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -102,8 +106,18 @@ describe('mergeExtensionParameters', () => {
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
{ extension: a, at: 'root', source: plugin, config: { foo: { bar: 1 } } },
|
||||
{ extension: b, at: 'root', source: plugin, config: { foo: { qux: 3 } } },
|
||||
{
|
||||
extension: a,
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
source: plugin,
|
||||
config: { foo: { bar: 1 } },
|
||||
},
|
||||
{
|
||||
extension: b,
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
source: plugin,
|
||||
config: { foo: { qux: 3 } },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -126,8 +140,8 @@ describe('mergeExtensionParameters', () => {
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
{ extension: b, at: 'root' },
|
||||
{ extension: a, at: 'root' },
|
||||
{ extension: b, attachTo: { id: 'root', input: 'default' } },
|
||||
{ extension: a, attachTo: { id: 'root', input: 'default' } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -315,14 +329,18 @@ describe('expandShorthandExtensionParameters', () => {
|
||||
expect(() =>
|
||||
run({ 'core.router': { id: 'some.id' } }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'at', 'disabled', 'config'"`,
|
||||
`"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('supports object at', () => {
|
||||
expect(run({ 'core.router': { at: 'other.root/inputs' } })).toEqual({
|
||||
it('supports object attachTo', () => {
|
||||
expect(
|
||||
run({
|
||||
'core.router': { attachTo: { id: 'other.root', input: 'inputs' } },
|
||||
}),
|
||||
).toEqual({
|
||||
id: 'core.router',
|
||||
at: 'other.root/inputs',
|
||||
attachTo: { id: 'other.root', input: 'inputs' },
|
||||
});
|
||||
expect(() =>
|
||||
run({
|
||||
@@ -331,7 +349,7 @@ describe('expandShorthandExtensionParameters', () => {
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'at', 'disabled', 'config'"`,
|
||||
`"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -369,7 +387,7 @@ describe('expandShorthandExtensionParameters', () => {
|
||||
expect(() =>
|
||||
run({ 'core.router': { foo: { settings: true } } }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'at', 'disabled', 'config'"`,
|
||||
`"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,12 +20,12 @@ import { JsonValue } from '@backstage/types';
|
||||
|
||||
export interface ExtensionParameters {
|
||||
id: string;
|
||||
at?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
config?: unknown;
|
||||
}
|
||||
|
||||
const knownExtensionParameters = ['at', 'disabled', 'config'];
|
||||
const knownExtensionParameters = ['attachTo', 'disabled', 'config'];
|
||||
|
||||
// Since we'll never merge arrays in config the config reader context
|
||||
// isn't too much of a help. Fall back to manual config reading logic
|
||||
@@ -143,15 +143,33 @@ export function expandShorthandExtensionParameters(
|
||||
throw new Error(errorMsg('value must be a boolean or object', id));
|
||||
}
|
||||
|
||||
const at = value.at;
|
||||
const attachTo = value.attachTo as { id: string; input: string } | undefined;
|
||||
const disabled = value.disabled;
|
||||
const config = value.config;
|
||||
|
||||
if (at !== undefined && typeof at !== 'string') {
|
||||
throw new Error(errorMsg('must be a string', id, 'at'));
|
||||
} else if (disabled !== undefined && typeof disabled !== 'boolean') {
|
||||
if (attachTo !== undefined) {
|
||||
if (
|
||||
attachTo === null ||
|
||||
typeof attachTo !== 'object' ||
|
||||
Array.isArray(attachTo)
|
||||
) {
|
||||
throw new Error(errorMsg('must be an object', id, 'attachTo'));
|
||||
}
|
||||
if (typeof attachTo.id !== 'string' || attachTo.id === '') {
|
||||
throw new Error(
|
||||
errorMsg('must be a non-empty string', id, 'attachTo.id'),
|
||||
);
|
||||
}
|
||||
if (typeof attachTo.input !== 'string' || attachTo.input === '') {
|
||||
throw new Error(
|
||||
errorMsg('must be a non-empty string', id, 'attachTo.input'),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (disabled !== undefined && typeof disabled !== 'boolean') {
|
||||
throw new Error(errorMsg('must be a boolean', id, 'disabled'));
|
||||
} else if (
|
||||
}
|
||||
if (
|
||||
config !== undefined &&
|
||||
(typeof config !== 'object' || config === null || Array.isArray(config))
|
||||
) {
|
||||
@@ -175,7 +193,7 @@ export function expandShorthandExtensionParameters(
|
||||
|
||||
return {
|
||||
id,
|
||||
at,
|
||||
attachTo,
|
||||
disabled,
|
||||
config,
|
||||
};
|
||||
@@ -184,7 +202,7 @@ export function expandShorthandExtensionParameters(
|
||||
export interface ExtensionInstanceParameters {
|
||||
extension: Extension<unknown>;
|
||||
source?: BackstagePlugin;
|
||||
at: string;
|
||||
attachTo: { id: string; input: string };
|
||||
config?: unknown;
|
||||
}
|
||||
|
||||
@@ -217,7 +235,7 @@ export function mergeExtensionParameters(options: {
|
||||
extension,
|
||||
params: {
|
||||
source,
|
||||
at: extension.at,
|
||||
attachTo: extension.attachTo,
|
||||
disabled: extension.disabled,
|
||||
config: undefined as unknown,
|
||||
},
|
||||
@@ -226,7 +244,7 @@ export function mergeExtensionParameters(options: {
|
||||
extension,
|
||||
params: {
|
||||
source: undefined,
|
||||
at: extension.at,
|
||||
attachTo: extension.attachTo,
|
||||
disabled: extension.disabled,
|
||||
config: undefined as unknown,
|
||||
},
|
||||
@@ -283,8 +301,8 @@ export function mergeExtensionParameters(options: {
|
||||
);
|
||||
if (existingIndex !== -1) {
|
||||
const existing = overrides[existingIndex];
|
||||
if (overrideParam.at) {
|
||||
existing.params.at = overrideParam.at;
|
||||
if (overrideParam.attachTo) {
|
||||
existing.params.attachTo = overrideParam.attachTo;
|
||||
}
|
||||
if (overrideParam.config) {
|
||||
// TODO: merge config?
|
||||
@@ -309,7 +327,7 @@ export function mergeExtensionParameters(options: {
|
||||
.filter(override => !override.params.disabled)
|
||||
.map(param => ({
|
||||
extension: param.extension,
|
||||
at: param.params.at,
|
||||
attachTo: param.params.attachTo,
|
||||
source: param.params.source,
|
||||
config: param.params.config,
|
||||
}));
|
||||
|
||||
@@ -136,7 +136,10 @@ export interface CreateExtensionOptions<
|
||||
TConfig,
|
||||
> {
|
||||
// (undocumented)
|
||||
at: string;
|
||||
attachTo: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
// (undocumented)
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
// (undocumented)
|
||||
@@ -182,7 +185,10 @@ export function createPageExtension<
|
||||
}
|
||||
) & {
|
||||
id: string;
|
||||
at?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
routeRef?: RouteRef;
|
||||
@@ -209,7 +215,10 @@ export interface Extension<TConfig> {
|
||||
// (undocumented)
|
||||
$$type: '@backstage/Extension';
|
||||
// (undocumented)
|
||||
at: string;
|
||||
attachTo: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
// (undocumented)
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
// (undocumented)
|
||||
@@ -324,7 +333,4 @@ export type PortableSchema<TOutput> = {
|
||||
parse: (input: unknown) => TOutput;
|
||||
schema: JsonObject;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export function useRouteRef(routeRef: RouteRef<any>): () => string;
|
||||
```
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('createApiExtension', () => {
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/Extension',
|
||||
id: 'apis.test',
|
||||
at: 'core/apis',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
configSchema: undefined,
|
||||
inputs: {},
|
||||
@@ -67,7 +67,7 @@ describe('createApiExtension', () => {
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/Extension',
|
||||
id: 'apis.test',
|
||||
at: 'core/apis',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
configSchema: undefined,
|
||||
inputs: {},
|
||||
|
||||
@@ -51,7 +51,7 @@ export function createApiExtension<
|
||||
|
||||
return createExtension({
|
||||
id: `apis.${apiRef.id}`,
|
||||
at: 'core/apis',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
inputs: extensionInputs,
|
||||
configSchema,
|
||||
output: {
|
||||
|
||||
@@ -31,7 +31,7 @@ export function createNavItemExtension(options: {
|
||||
const { id, routeRef, title, icon } = options;
|
||||
return createExtension({
|
||||
id,
|
||||
at: 'core.nav/items',
|
||||
attachTo: { id: 'core.nav', input: 'items' },
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
title: z.string().default(title),
|
||||
|
||||
@@ -35,7 +35,7 @@ describe('createPageExtension', () => {
|
||||
).toEqual({
|
||||
$$type: '@backstage/Extension',
|
||||
id: 'test',
|
||||
at: 'core.routes/routes',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
configSchema: expect.anything(),
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
@@ -50,7 +50,7 @@ describe('createPageExtension', () => {
|
||||
expect(
|
||||
createPageExtension({
|
||||
id: 'test',
|
||||
at: 'other/place',
|
||||
attachTo: { id: 'other', input: 'place' },
|
||||
disabled: true,
|
||||
configSchema,
|
||||
inputs: {
|
||||
@@ -63,7 +63,7 @@ describe('createPageExtension', () => {
|
||||
).toEqual({
|
||||
$$type: '@backstage/Extension',
|
||||
id: 'test',
|
||||
at: 'other/place',
|
||||
attachTo: { id: 'other', input: 'place' },
|
||||
configSchema: expect.anything(),
|
||||
disabled: true,
|
||||
inputs: {
|
||||
@@ -88,7 +88,7 @@ describe('createPageExtension', () => {
|
||||
).toEqual({
|
||||
$$type: '@backstage/Extension',
|
||||
id: 'test',
|
||||
at: 'core.routes/routes',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
configSchema: expect.anything(),
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
|
||||
@@ -44,7 +44,7 @@ export function createPageExtension<
|
||||
}
|
||||
) & {
|
||||
id: string;
|
||||
at?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
routeRef?: RouteRef;
|
||||
@@ -63,7 +63,7 @@ export function createPageExtension<
|
||||
|
||||
return createExtension({
|
||||
id: options.id,
|
||||
at: options.at ?? 'core.routes/routes',
|
||||
attachTo: options.attachTo ?? { id: 'core.routes', input: 'routes' },
|
||||
disabled: options.disabled,
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
|
||||
@@ -21,7 +21,7 @@ import { AppTheme } from '@backstage/core-plugin-api';
|
||||
export function createThemeExtension(theme: AppTheme) {
|
||||
return createExtension({
|
||||
id: `themes.${theme.id}`,
|
||||
at: 'core/themes',
|
||||
attachTo: { id: 'core', input: 'themes' },
|
||||
output: {
|
||||
theme: coreExtensionData.theme,
|
||||
},
|
||||
|
||||
@@ -24,4 +24,3 @@ export * from './components';
|
||||
export * from './extensions';
|
||||
export * from './schema';
|
||||
export * from './wiring';
|
||||
export * from './routing';
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 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 { useRouteRef } from './useRouteRef';
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023 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/core-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-forbidden-package-imports
|
||||
import { RoutingContext } from '@backstage/frontend-app-api/src/routing/RoutingContext';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
/** @public */
|
||||
export function useRouteRef(routeRef: RouteRef<any>): () => string {
|
||||
const { pathname } = useLocation();
|
||||
const resolver = useContext(RoutingContext);
|
||||
|
||||
const routeFunc = useMemo(
|
||||
() => resolver && resolver.resolve(routeRef, { pathname }),
|
||||
[resolver, routeRef, pathname],
|
||||
);
|
||||
|
||||
if (!routeFunc) {
|
||||
throw new Error(`Failed to resolve routeRef ${routeRef}`);
|
||||
}
|
||||
|
||||
return routeFunc;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ describe('createExtension', () => {
|
||||
it('should create an extension with a simple output', () => {
|
||||
const extension = createExtension({
|
||||
id: 'test',
|
||||
at: 'root',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: {
|
||||
foo: stringData,
|
||||
},
|
||||
@@ -56,7 +56,7 @@ describe('createExtension', () => {
|
||||
it('should create an extension with a some optional output', () => {
|
||||
const extension = createExtension({
|
||||
id: 'test',
|
||||
at: 'root',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: {
|
||||
foo: stringData,
|
||||
bar: stringData.optional(),
|
||||
@@ -94,7 +94,7 @@ describe('createExtension', () => {
|
||||
it('should create an extension with input', () => {
|
||||
const extension = createExtension({
|
||||
id: 'test',
|
||||
at: 'root',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
inputs: {
|
||||
mixed: createExtensionInput({
|
||||
required: stringData,
|
||||
|
||||
@@ -80,7 +80,7 @@ export interface CreateExtensionOptions<
|
||||
TConfig,
|
||||
> {
|
||||
id: string;
|
||||
at: string;
|
||||
attachTo: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
output: TOutput;
|
||||
@@ -97,7 +97,7 @@ export interface CreateExtensionOptions<
|
||||
export interface Extension<TConfig> {
|
||||
$$type: '@backstage/Extension';
|
||||
id: string;
|
||||
at: string;
|
||||
attachTo: { id: string; input: string };
|
||||
disabled: boolean;
|
||||
inputs: AnyExtensionInputMap;
|
||||
output: AnyExtensionDataMap;
|
||||
|
||||
@@ -30,7 +30,7 @@ const nameExtensionDataRef = createExtensionDataRef<string>('name');
|
||||
|
||||
const TechRadarPage = createExtension({
|
||||
id: 'plugin.techradar.page',
|
||||
at: 'test.output/names',
|
||||
attachTo: { id: 'test.output', input: 'names' },
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
@@ -41,7 +41,7 @@ const TechRadarPage = createExtension({
|
||||
|
||||
const CatalogPage = createExtension({
|
||||
id: 'plugin.catalog.page',
|
||||
at: 'test.output/names',
|
||||
attachTo: { id: 'test.output', input: 'names' },
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
@@ -55,7 +55,7 @@ const CatalogPage = createExtension({
|
||||
|
||||
const TechDocsAddon = createExtension({
|
||||
id: 'plugin.techdocs.addon.example',
|
||||
at: 'plugin.techdocs.page/addons',
|
||||
attachTo: { id: 'plugin.techdocs.page', input: 'addons' },
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
@@ -69,7 +69,7 @@ const TechDocsAddon = createExtension({
|
||||
|
||||
const TechDocsPage = createExtension({
|
||||
id: 'plugin.techdocs.page',
|
||||
at: 'test.output/names',
|
||||
attachTo: { id: 'test.output', input: 'names' },
|
||||
inputs: {
|
||||
addons: createExtensionInput({
|
||||
name: nameExtensionDataRef,
|
||||
@@ -85,7 +85,7 @@ const TechDocsPage = createExtension({
|
||||
|
||||
const outputExtension = createExtension({
|
||||
id: 'test.output',
|
||||
at: 'root',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
inputs: {
|
||||
names: createExtensionInput({
|
||||
name: nameExtensionDataRef,
|
||||
|
||||
@@ -33,6 +33,9 @@ import { ResultHighlight } from '@backstage/plugin-search-common';
|
||||
import { HighlightedSearchResultText } from '@backstage/plugin-search-react';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
item: {
|
||||
display: 'flex',
|
||||
},
|
||||
flexContainer: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
@@ -66,59 +69,63 @@ export function AdrSearchResultListItem(props: AdrSearchResultListItemProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem alignItems="flex-start" className={classes.flexContainer}>
|
||||
<ListItem alignItems="flex-start" className={classes.item}>
|
||||
{icon && <ListItemIcon>{icon}</ListItemIcon>}
|
||||
<ListItemText
|
||||
className={classes.itemText}
|
||||
primaryTypographyProps={{ variant: 'h6' }}
|
||||
primary={
|
||||
<Link noTrack to={result.location}>
|
||||
{highlight?.fields.title ? (
|
||||
<HighlightedSearchResultText
|
||||
text={highlight?.fields.title || ''}
|
||||
preTag={highlight?.preTag || ''}
|
||||
postTag={highlight?.postTag || ''}
|
||||
/>
|
||||
) : (
|
||||
result.title
|
||||
)}
|
||||
</Link>
|
||||
}
|
||||
secondary={
|
||||
<Typography
|
||||
component="span"
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: lineClamp,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{highlight?.fields.text ? (
|
||||
<HighlightedSearchResultText
|
||||
text={highlight.fields.text}
|
||||
preTag={highlight.preTag}
|
||||
postTag={highlight.postTag}
|
||||
/>
|
||||
) : (
|
||||
result.text
|
||||
)}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
<Box>
|
||||
<Chip
|
||||
label={`Entity: ${
|
||||
result.entityTitle ??
|
||||
humanizeEntityRef(parseEntityRef(result.entityRef))
|
||||
}`}
|
||||
size="small"
|
||||
<div className={classes.flexContainer}>
|
||||
<ListItemText
|
||||
className={classes.itemText}
|
||||
primaryTypographyProps={{ variant: 'h6' }}
|
||||
primary={
|
||||
<Link noTrack to={result.location}>
|
||||
{highlight?.fields.title ? (
|
||||
<HighlightedSearchResultText
|
||||
text={highlight?.fields.title || ''}
|
||||
preTag={highlight?.preTag || ''}
|
||||
postTag={highlight?.postTag || ''}
|
||||
/>
|
||||
) : (
|
||||
result.title
|
||||
)}
|
||||
</Link>
|
||||
}
|
||||
secondary={
|
||||
<Typography
|
||||
component="span"
|
||||
style={{
|
||||
display: '-webkit-box',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: lineClamp,
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{highlight?.fields.text ? (
|
||||
<HighlightedSearchResultText
|
||||
text={highlight.fields.text}
|
||||
preTag={highlight.preTag}
|
||||
postTag={highlight.postTag}
|
||||
/>
|
||||
) : (
|
||||
result.text
|
||||
)}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
{result.status && (
|
||||
<Chip label={`Status: ${result.status}`} size="small" />
|
||||
)}
|
||||
{result.date && <Chip label={`Date: ${result.date}`} size="small" />}
|
||||
</Box>
|
||||
<Box>
|
||||
<Chip
|
||||
label={`Entity: ${
|
||||
result.entityTitle ??
|
||||
humanizeEntityRef(parseEntityRef(result.entityRef))
|
||||
}`}
|
||||
size="small"
|
||||
/>
|
||||
{result.status && (
|
||||
<Chip label={`Status: ${result.status}`} size="small" />
|
||||
)}
|
||||
{result.date && (
|
||||
<Chip label={`Date: ${result.date}`} size="small" />
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
</ListItem>
|
||||
<Divider component="li" />
|
||||
</>
|
||||
|
||||
@@ -69,7 +69,6 @@
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/types": "workspace:^",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"mock-fs": "^5.2.0",
|
||||
"msw": "^1.0.0",
|
||||
"node-fetch": "^2.6.7",
|
||||
"supertest": "^6.1.3"
|
||||
|
||||
@@ -14,40 +14,40 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import mockFs from 'mock-fs';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { findStaticAssets } from './findStaticAssets';
|
||||
|
||||
describe('findStaticAssets', () => {
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
mockDir.clear();
|
||||
});
|
||||
|
||||
it('should find assets', async () => {
|
||||
mockFs({
|
||||
'/test': {
|
||||
'a.js': 'alert("hello")',
|
||||
'a.js.map': '',
|
||||
'b.js': 'b',
|
||||
'b.js.map': '',
|
||||
js: {
|
||||
'd.js': 'd',
|
||||
'd.js.map': '',
|
||||
x: {
|
||||
mockDir.setContent({
|
||||
'a.js': 'alert("hello")',
|
||||
'a.js.map': '',
|
||||
'b.js': 'b',
|
||||
'b.js.map': '',
|
||||
js: {
|
||||
'd.js': 'd',
|
||||
'd.js.map': '',
|
||||
x: {
|
||||
'e.map': '',
|
||||
y: {
|
||||
'e.map': '',
|
||||
y: {
|
||||
z: {
|
||||
'e.js': 'e',
|
||||
'e.map': '',
|
||||
z: {
|
||||
'e.js': 'e',
|
||||
'e.map': '',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
styles: { 'c.css': 'body { color: red; }' },
|
||||
},
|
||||
styles: { 'c.css': 'body { color: red; }' },
|
||||
});
|
||||
|
||||
const assets = await findStaticAssets('/test');
|
||||
const assets = await findStaticAssets(mockDir.path);
|
||||
expect(assets.length).toBe(5);
|
||||
expect(assets.map(a => a.path)).toEqual(
|
||||
expect.arrayContaining([
|
||||
|
||||
@@ -14,33 +14,36 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import mockFs from 'mock-fs';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
import { mockServices, startTestBackend } from '@backstage/backend-test-utils';
|
||||
import {
|
||||
createMockDirectory,
|
||||
mockServices,
|
||||
startTestBackend,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { appPlugin } from './appPlugin';
|
||||
import { createRootLogger } from '@backstage/backend-common';
|
||||
import { overridePackagePathResolution } from '@backstage/backend-common/testUtils';
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
overridePackagePathResolution({
|
||||
packageName: 'app',
|
||||
path: mockDir.path,
|
||||
});
|
||||
|
||||
// Make sure root logger is initialized ahead of FS mock
|
||||
createRootLogger();
|
||||
|
||||
describe('appPlugin', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[resolvePath(process.cwd(), 'node_modules/app')]: {
|
||||
'package.json': '{}',
|
||||
dist: {
|
||||
static: {},
|
||||
'index.html': 'winning',
|
||||
},
|
||||
mockDir.setContent({
|
||||
'package.json': '{}',
|
||||
dist: {
|
||||
static: {},
|
||||
'index.html': 'winning',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('boots', async () => {
|
||||
const { server } = await startTestBackend({
|
||||
features: [
|
||||
|
||||
@@ -372,7 +372,7 @@ describe('PlaceholderProcessor', () => {
|
||||
() => {},
|
||||
),
|
||||
).rejects.toThrow(
|
||||
/^Placeholder \$text could not form a URL out of \.\/a\/b\/catalog-info\.yaml and \.\.\/c\/catalog-info\.yaml, TypeError \[ERR_INVALID_URL\]/,
|
||||
/^Placeholder \$text could not form a URL out of \.\/a\/b\/catalog-info\.yaml and \.\.\/c\/catalog-info\.yaml, TypeError/,
|
||||
);
|
||||
|
||||
expect(reader.readUrl).not.toHaveBeenCalled();
|
||||
|
||||
@@ -34,7 +34,44 @@ jest.mock('react-router-dom', () => ({
|
||||
useNavigate: () => navigate,
|
||||
}));
|
||||
|
||||
describe('<CatalogGraphPage/>', () => {
|
||||
/*
|
||||
The tests in this file have been disabled for the following error:
|
||||
|
||||
TypeError: Cannot read properties of null (reading 'document')
|
||||
|
||||
at document (../../../node_modules/d3-drag/src/nodrag.js:5:19)
|
||||
at SVGSVGElement.mousedowned (../../../node_modules/d3-zoom/src/zoom.js:279:16)
|
||||
at SVGSVGElement.call (../../../node_modules/d3-selection/src/selection/on.js:3:14)
|
||||
at SVGSVGElement.callTheUserObjectsOperation (../../../node_modules/jsdom/lib/jsdom/living/generated/EventListener.js:26:30)
|
||||
at innerInvokeEventListeners (../../../node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:350:25)
|
||||
at invokeEventListeners (../../../node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:286:3)
|
||||
at SVGElementImpl._dispatch (../../../node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:233:9)
|
||||
at SVGElementImpl.dispatchEvent (../../../node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:104:17)
|
||||
at SVGElement.dispatchEvent (../../../node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:241:34)
|
||||
at ../../../node_modules/@testing-library/user-event/dist/cjs/event/dispatchEvent.js:47:43
|
||||
at cb (../../../node_modules/@testing-library/react/dist/pure.js:66:16)
|
||||
at batchedUpdates$1 (../../../node_modules/react-dom/cjs/react-dom.development.js:22380:12)
|
||||
at act (../../../node_modules/react-dom/cjs/react-dom-test-utils.development.js:1042:14)
|
||||
at Object.eventWrapper (../../../node_modules/@testing-library/react/dist/pure.js:65:26)
|
||||
at Object.wrapEvent (../../../node_modules/@testing-library/user-event/dist/cjs/event/wrapEvent.js:29:24)
|
||||
at Object.dispatchEvent (../../../node_modules/@testing-library/user-event/dist/cjs/event/dispatchEvent.js:47:22)
|
||||
at Object.dispatchUIEvent (../../../node_modules/@testing-library/user-event/dist/cjs/event/dispatchEvent.js:24:26)
|
||||
at Mouse.down (../../../node_modules/@testing-library/user-event/dist/cjs/system/pointer/mouse.js:83:34)
|
||||
at PointerHost.press (../../../node_modules/@testing-library/user-event/dist/cjs/system/pointer/index.js:39:24)
|
||||
at pointerAction (../../../node_modules/@testing-library/user-event/dist/cjs/pointer/index.js:59:43)
|
||||
at Object.pointer (../../../node_modules/@testing-library/user-event/dist/cjs/pointer/index.js:35:15)
|
||||
at ../../../node_modules/@testing-library/react/dist/pure.js:59:16
|
||||
|
||||
This has started happening after upgrading to the later version of @testing-library/user-event, and the d3-drag library
|
||||
where it happens seems to be unmaintained. Skipping for now.
|
||||
|
||||
https://github.com/d3/d3-drag/issues/79#issuecomment-1631409544
|
||||
|
||||
https://github.com/d3/d3-drag/issues/89
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line jest/no-disabled-tests
|
||||
describe.skip('<CatalogGraphPage/>', () => {
|
||||
let wrapper: JSX.Element;
|
||||
const entityC = {
|
||||
apiVersion: 'a',
|
||||
|
||||
@@ -86,7 +86,7 @@ export function createEndpointExtension<TConfig extends {}>(options: {
|
||||
}) {
|
||||
return createExtension({
|
||||
id: `apis.plugin.graphiql.browse.${options.id}`,
|
||||
at: 'apis.plugin.graphiql.browse/endpoints',
|
||||
attachTo: { id: 'apis.plugin.graphiql.browse', input: 'endpoints' },
|
||||
configSchema: options.configSchema,
|
||||
disabled: options.disabled ?? false,
|
||||
output: {
|
||||
|
||||
@@ -89,7 +89,6 @@
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/aws4": "^1.5.1",
|
||||
"mock-fs": "^5.2.0",
|
||||
"msw": "^1.0.0",
|
||||
"supertest": "^6.1.3",
|
||||
"ws": "^8.13.0"
|
||||
|
||||
@@ -13,8 +13,37 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { ServiceAccountStrategy } from './ServiceAccountStrategy';
|
||||
import mockFs from 'mock-fs';
|
||||
|
||||
const mockDir = createMockDirectory({
|
||||
content: {
|
||||
'token.txt': 'in-cluster-token',
|
||||
},
|
||||
});
|
||||
|
||||
jest.mock('@kubernetes/client-node', () => ({
|
||||
KubeConfig: class {
|
||||
#loaded = false;
|
||||
loadFromCluster() {
|
||||
this.#loaded = true;
|
||||
}
|
||||
getCurrentUser() {
|
||||
if (!this.#loaded) {
|
||||
throw new Error('loadFromCluster not called');
|
||||
}
|
||||
return {
|
||||
authProvider: {
|
||||
config: {
|
||||
get tokenFile() {
|
||||
return mockDir.resolve('token.txt');
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ServiceAccountStrategy', () => {
|
||||
describe('#getCredential', () => {
|
||||
@@ -32,16 +61,9 @@ describe('ServiceAccountStrategy', () => {
|
||||
token: 'from config',
|
||||
});
|
||||
});
|
||||
describe('when serviceAccountToken is absent from config', () => {
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
describe('when serviceAccountToken is absent from config', () => {
|
||||
it('reads in-cluster token', async () => {
|
||||
mockFs({
|
||||
'/var/run/secrets/kubernetes.io/serviceaccount/token':
|
||||
'in-cluster-token',
|
||||
});
|
||||
const strategy = new ServiceAccountStrategy();
|
||||
|
||||
const credential = await strategy.getCredential({
|
||||
|
||||
@@ -26,8 +26,17 @@ import {
|
||||
rest,
|
||||
} from 'msw';
|
||||
import { setupServer } from 'msw/node';
|
||||
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
|
||||
import mockFs from 'mock-fs';
|
||||
import {
|
||||
createMockDirectory,
|
||||
setupRequestMockHandlers,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { Config } from '@kubernetes/client-node';
|
||||
|
||||
const mockCertDir = createMockDirectory({
|
||||
content: {
|
||||
'ca.crt': 'MOCKCA',
|
||||
},
|
||||
});
|
||||
|
||||
const OBJECTS_TO_FETCH = new Set<ObjectToFetch>([
|
||||
{
|
||||
@@ -728,13 +737,7 @@ describe('KubernetesFetcher', () => {
|
||||
expect(agent.options.ca).toBeUndefined();
|
||||
});
|
||||
describe('with a CA file on disk', () => {
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
it('should trust contents of specified caFile', async () => {
|
||||
mockFs({
|
||||
'/path/to/ca.crt': 'MOCKCA',
|
||||
});
|
||||
worker.use(
|
||||
rest.get('https://localhost:9999/api/v1/pods', (req, res, ctx) =>
|
||||
res(
|
||||
@@ -752,7 +755,7 @@ describe('KubernetesFetcher', () => {
|
||||
name: 'cluster1',
|
||||
url: 'https://localhost:9999',
|
||||
authMetadata: {},
|
||||
caFile: '/path/to/ca.crt',
|
||||
caFile: mockCertDir.resolve('ca.crt'),
|
||||
},
|
||||
credential: { type: 'bearer token', token: 'token' },
|
||||
objectTypesToFetch: new Set<ObjectToFetch>([
|
||||
@@ -899,17 +902,18 @@ describe('KubernetesFetcher', () => {
|
||||
describe('Backstage running on k8s', () => {
|
||||
const initialHost = process.env.KUBERNETES_SERVICE_HOST;
|
||||
const initialPort = process.env.KUBERNETES_SERVICE_PORT;
|
||||
const initialCaPath = Config.SERVICEACCOUNT_CA_PATH;
|
||||
|
||||
afterEach(() => {
|
||||
process.env.KUBERNETES_SERVICE_HOST = initialHost;
|
||||
process.env.KUBERNETES_SERVICE_PORT = initialPort;
|
||||
mockFs.restore();
|
||||
Config.SERVICEACCOUNT_CA_PATH = initialCaPath;
|
||||
});
|
||||
|
||||
it('makes in-cluster requests when cluster details has no token', async () => {
|
||||
process.env.KUBERNETES_SERVICE_HOST = '10.10.10.10';
|
||||
process.env.KUBERNETES_SERVICE_PORT = '443';
|
||||
mockFs({
|
||||
'/var/run/secrets/kubernetes.io/serviceaccount/ca.crt': '',
|
||||
});
|
||||
Config.SERVICEACCOUNT_CA_PATH = mockCertDir.resolve('ca.crt');
|
||||
worker.use(
|
||||
rest.get('https://10.10.10.10/api/v1/pods', (req, res, ctx) =>
|
||||
res(
|
||||
|
||||
@@ -22,16 +22,56 @@ import { newRelicApiRef, NewRelicApplications } from '../../api';
|
||||
import { Progress, Table, TableColumn } from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
const sortNumeric =
|
||||
<F extends string>(field: F) =>
|
||||
(a: { [key in F]: number }, b: { [key in F]: number }) => {
|
||||
return a[field] - b[field];
|
||||
};
|
||||
|
||||
type NewRelicTableData = {
|
||||
name: string;
|
||||
responseTime: number;
|
||||
throughput: number;
|
||||
errorRate: number;
|
||||
instanceCount: number;
|
||||
apdexScore: number;
|
||||
};
|
||||
|
||||
export const NewRelicAPMTable = ({ applications }: NewRelicApplications) => {
|
||||
const columns: TableColumn[] = [
|
||||
{ title: 'Application', field: 'name' },
|
||||
{ title: 'Response Time (ms)', field: 'responseTime' },
|
||||
{ title: 'Throughput (rpm)', field: 'throughput' },
|
||||
{ title: 'Error Rate (%)', field: 'errorRate' },
|
||||
{ title: 'Instance Count', field: 'instanceCount' },
|
||||
{ title: 'Apdex', field: 'apdexScore' },
|
||||
const columns: TableColumn<NewRelicTableData>[] = [
|
||||
{ title: 'Application', field: 'name', searchable: true },
|
||||
{
|
||||
title: 'Response Time (ms)',
|
||||
field: 'responseTime',
|
||||
customSort: sortNumeric('responseTime'),
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
title: 'Throughput (rpm)',
|
||||
field: 'throughput',
|
||||
customSort: sortNumeric('throughput'),
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
title: 'Error Rate (%)',
|
||||
field: 'errorRate',
|
||||
customSort: sortNumeric('errorRate'),
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
title: 'Instance Count',
|
||||
field: 'instanceCount',
|
||||
customSort: sortNumeric('instanceCount'),
|
||||
searchable: false,
|
||||
},
|
||||
{
|
||||
title: 'Apdex',
|
||||
field: 'apdexScore',
|
||||
customSort: sortNumeric('apdexScore'),
|
||||
searchable: false,
|
||||
},
|
||||
];
|
||||
const data = applications.map(app => {
|
||||
const data: Array<NewRelicTableData> = applications.map(app => {
|
||||
const { name, application_summary: applicationSummary } = app;
|
||||
const {
|
||||
response_time: responseTime,
|
||||
|
||||
@@ -107,13 +107,11 @@
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/git-url-parse": "^9.0.0",
|
||||
"@types/libsodium-wrappers": "^0.7.10",
|
||||
"@types/mock-fs": "^4.13.0",
|
||||
"@types/nunjucks": "^3.1.4",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"esbuild": "^0.19.0",
|
||||
"jest-when": "^3.1.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"msw": "^1.0.0",
|
||||
"supertest": "^6.1.3",
|
||||
"wait-for-expect": "^3.0.2",
|
||||
|
||||
@@ -14,29 +14,25 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import mockFs from 'mock-fs';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { deserializeDirectoryContents } from './deserializeDirectoryContents';
|
||||
import { serializeDirectoryContents } from './serializeDirectoryContents';
|
||||
|
||||
describe('deserializeDirectoryContents', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
root: {},
|
||||
});
|
||||
});
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
beforeEach(() => {
|
||||
mockDir.clear();
|
||||
});
|
||||
|
||||
it('deserializes contents into a directory', async () => {
|
||||
await deserializeDirectoryContents('root', [
|
||||
await deserializeDirectoryContents(mockDir.path, [
|
||||
{
|
||||
path: 'a.txt',
|
||||
content: Buffer.from('a', 'utf8'),
|
||||
},
|
||||
]);
|
||||
await expect(serializeDirectoryContents('root')).resolves.toEqual([
|
||||
await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([
|
||||
{
|
||||
path: 'a.txt',
|
||||
content: Buffer.from('a', 'utf8'),
|
||||
@@ -47,7 +43,7 @@ describe('deserializeDirectoryContents', () => {
|
||||
});
|
||||
|
||||
it('deserializes contents into a deep directory structure', async () => {
|
||||
await deserializeDirectoryContents('root', [
|
||||
await deserializeDirectoryContents(mockDir.path, [
|
||||
{
|
||||
path: 'a.txt',
|
||||
content: Buffer.from('a', 'utf8'),
|
||||
@@ -61,7 +57,7 @@ describe('deserializeDirectoryContents', () => {
|
||||
content: Buffer.from('c', 'utf8'),
|
||||
},
|
||||
]);
|
||||
await expect(serializeDirectoryContents('root')).resolves.toEqual([
|
||||
await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([
|
||||
{
|
||||
path: 'a.txt',
|
||||
content: Buffer.from('a', 'utf8'),
|
||||
|
||||
@@ -14,13 +14,11 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import { serializeDirectoryContents } from './serializeDirectoryContents';
|
||||
import mockFs from 'mock-fs';
|
||||
|
||||
describe('serializeDirectoryContents', () => {
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
it('should list files in this directory', async () => {
|
||||
await expect(serializeDirectoryContents(__dirname)).resolves.toEqual(
|
||||
@@ -54,25 +52,23 @@ describe('serializeDirectoryContents', () => {
|
||||
});
|
||||
|
||||
it('should list files in a mock directory', async () => {
|
||||
mockFs({
|
||||
root: {
|
||||
'a.txt': 'a',
|
||||
b: {
|
||||
'b1.txt': 'b1',
|
||||
'b2.txt': 'b2',
|
||||
},
|
||||
c: {
|
||||
c1: {
|
||||
'c11.txt': 'c11',
|
||||
c11: {
|
||||
'c111.txt': 'c111',
|
||||
},
|
||||
mockDir.setContent({
|
||||
'a.txt': 'a',
|
||||
b: {
|
||||
'b1.txt': 'b1',
|
||||
'b2.txt': 'b2',
|
||||
},
|
||||
c: {
|
||||
c1: {
|
||||
'c11.txt': 'c11',
|
||||
c11: {
|
||||
'c111.txt': 'c111',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(serializeDirectoryContents('root')).resolves.toEqual([
|
||||
await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([
|
||||
{
|
||||
path: 'a.txt',
|
||||
executable: false,
|
||||
@@ -107,16 +103,12 @@ describe('serializeDirectoryContents', () => {
|
||||
});
|
||||
|
||||
it('should ignore symlinked files', async () => {
|
||||
mockFs({
|
||||
root: {
|
||||
'a.txt': 'some text',
|
||||
sym: mockFs.symlink({
|
||||
path: './a.txt',
|
||||
}),
|
||||
},
|
||||
mockDir.setContent({
|
||||
'a.txt': 'some text',
|
||||
sym: ctx => ctx.symlink('./a.txt'),
|
||||
});
|
||||
|
||||
await expect(serializeDirectoryContents('root')).resolves.toEqual([
|
||||
await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([
|
||||
{
|
||||
path: 'a.txt',
|
||||
executable: false,
|
||||
@@ -127,18 +119,14 @@ describe('serializeDirectoryContents', () => {
|
||||
});
|
||||
|
||||
it('should pick up broken symlinks', async () => {
|
||||
mockFs({
|
||||
root: {
|
||||
'b.txt': mockFs.symlink({
|
||||
path: './a.txt',
|
||||
}),
|
||||
},
|
||||
mockDir.setContent({
|
||||
'b.txt': ctx => ctx.symlink('./a.txt'),
|
||||
});
|
||||
|
||||
await expect(serializeDirectoryContents('root')).resolves.toEqual([
|
||||
await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([
|
||||
{
|
||||
path: 'b.txt',
|
||||
executable: false,
|
||||
executable: true,
|
||||
symlink: true,
|
||||
content: Buffer.from('./a.txt', 'utf8'),
|
||||
},
|
||||
@@ -146,19 +134,15 @@ describe('serializeDirectoryContents', () => {
|
||||
});
|
||||
|
||||
it('should ignore symlinked folder files', async () => {
|
||||
mockFs({
|
||||
root: {
|
||||
'a.txt': 'some text',
|
||||
linkme: {
|
||||
'b.txt': 'lols',
|
||||
},
|
||||
sym: mockFs.symlink({
|
||||
path: './linkme',
|
||||
}),
|
||||
mockDir.setContent({
|
||||
'a.txt': 'some text',
|
||||
linkme: {
|
||||
'b.txt': 'lols',
|
||||
},
|
||||
sym: ctx => ctx.symlink('./linkme'),
|
||||
});
|
||||
|
||||
await expect(serializeDirectoryContents('root')).resolves.toEqual([
|
||||
await expect(serializeDirectoryContents(mockDir.path)).resolves.toEqual([
|
||||
{
|
||||
path: 'a.txt',
|
||||
executable: false,
|
||||
@@ -175,16 +159,14 @@ describe('serializeDirectoryContents', () => {
|
||||
});
|
||||
|
||||
it('should ignore gitignored files', async () => {
|
||||
mockFs({
|
||||
root: {
|
||||
'.gitignore': '*.txt',
|
||||
'a.txt': 'a',
|
||||
'a.log': 'a',
|
||||
},
|
||||
mockDir.setContent({
|
||||
'.gitignore': '*.txt',
|
||||
'a.txt': 'a',
|
||||
'a.log': 'a',
|
||||
});
|
||||
|
||||
await expect(
|
||||
serializeDirectoryContents('root', {
|
||||
serializeDirectoryContents(mockDir.path, {
|
||||
gitignore: true,
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
@@ -204,26 +186,24 @@ describe('serializeDirectoryContents', () => {
|
||||
});
|
||||
|
||||
it('should use custom glob patterns', async () => {
|
||||
mockFs({
|
||||
root: {
|
||||
'.a': 'a',
|
||||
'a.log': 'a',
|
||||
'a.txt': 'a',
|
||||
b: {
|
||||
'.b': 'b',
|
||||
'b.log': 'b',
|
||||
'b.txt': 'b',
|
||||
},
|
||||
c: {
|
||||
'.c': 'c',
|
||||
'c.log': 'c',
|
||||
'c.txt': 'c',
|
||||
},
|
||||
mockDir.setContent({
|
||||
'.a': 'a',
|
||||
'a.log': 'a',
|
||||
'a.txt': 'a',
|
||||
b: {
|
||||
'.b': 'b',
|
||||
'b.log': 'b',
|
||||
'b.txt': 'b',
|
||||
},
|
||||
c: {
|
||||
'.c': 'c',
|
||||
'c.log': 'c',
|
||||
'c.txt': 'c',
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
serializeDirectoryContents('root', {
|
||||
serializeDirectoryContents(mockDir.path, {
|
||||
gitignore: true,
|
||||
globPatterns: ['**/*.txt', '*/.?', '*/*.log', '!c/**/.*', '!b/*.log'],
|
||||
}).then(files => files.sort((a, b) => a.path.localeCompare(b.path))),
|
||||
|
||||
+9
-12
@@ -15,44 +15,41 @@
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import { Writable } from 'stream';
|
||||
import { createDebugLogAction } from './log';
|
||||
import { join } from 'path';
|
||||
import yaml from 'yaml';
|
||||
import { examples } from './log.examples';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('debug:log examples', () => {
|
||||
const logStream = {
|
||||
write: jest.fn(),
|
||||
} as jest.Mocked<Partial<Writable>> as jest.Mocked<Writable>;
|
||||
|
||||
const mockTmpDir = os.tmpdir();
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = mockDir.resolve('workspace');
|
||||
|
||||
const mockContext = {
|
||||
input: {},
|
||||
baseUrl: 'somebase',
|
||||
workspacePath: mockTmpDir,
|
||||
workspacePath,
|
||||
logger: getVoidLogger(),
|
||||
logStream,
|
||||
output: jest.fn(),
|
||||
createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir),
|
||||
createTemporaryDirectory: jest.fn(),
|
||||
};
|
||||
|
||||
const action = createDebugLogAction();
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[`${mockContext.workspacePath}/README.md`]: '',
|
||||
[`${mockContext.workspacePath}/a-directory/index.md`]: '',
|
||||
mockDir.setContent({
|
||||
[`${workspacePath}/README.md`]: '',
|
||||
[`${workspacePath}/a-directory/index.md`]: '',
|
||||
});
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should log message', async () => {
|
||||
const context = {
|
||||
...mockContext,
|
||||
|
||||
@@ -15,43 +15,40 @@
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import { Writable } from 'stream';
|
||||
import { createDebugLogAction } from './log';
|
||||
import { join } from 'path';
|
||||
import yaml from 'yaml';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('debug:log', () => {
|
||||
const logStream = {
|
||||
write: jest.fn(),
|
||||
} as jest.Mocked<Partial<Writable>> as jest.Mocked<Writable>;
|
||||
|
||||
const mockTmpDir = os.tmpdir();
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = mockDir.resolve('workspace');
|
||||
|
||||
const mockContext = {
|
||||
input: {},
|
||||
baseUrl: 'somebase',
|
||||
workspacePath: mockTmpDir,
|
||||
workspacePath,
|
||||
logger: getVoidLogger(),
|
||||
logStream,
|
||||
output: jest.fn(),
|
||||
createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir),
|
||||
createTemporaryDirectory: jest.fn(),
|
||||
};
|
||||
|
||||
const action = createDebugLogAction();
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[`${mockContext.workspacePath}/README.md`]: '',
|
||||
[`${mockContext.workspacePath}/a-directory/index.md`]: '',
|
||||
});
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should do nothing', async () => {
|
||||
await action.handler(mockContext);
|
||||
|
||||
|
||||
+6
-9
@@ -15,12 +15,11 @@
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import mockFs from 'mock-fs';
|
||||
import { createWaitAction } from './wait';
|
||||
import { Writable } from 'stream';
|
||||
import os from 'os';
|
||||
import { examples } from './wait.examples';
|
||||
import yaml from 'yaml';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('debug:wait examples', () => {
|
||||
const action = createWaitAction();
|
||||
@@ -29,25 +28,23 @@ describe('debug:wait examples', () => {
|
||||
write: jest.fn(),
|
||||
} as jest.Mocked<Partial<Writable>> as jest.Mocked<Writable>;
|
||||
|
||||
const mockTmpDir = os.tmpdir();
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = mockDir.resolve('workspace');
|
||||
|
||||
const mockContext = {
|
||||
input: {},
|
||||
baseUrl: 'somebase',
|
||||
workspacePath: mockTmpDir,
|
||||
workspacePath,
|
||||
logger: getVoidLogger(),
|
||||
logStream,
|
||||
output: jest.fn(),
|
||||
createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir),
|
||||
createTemporaryDirectory: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should wait for specified period of seconds', async () => {
|
||||
const context = {
|
||||
...mockContext,
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import mockFs from 'mock-fs';
|
||||
import { createWaitAction } from './wait';
|
||||
import { Writable } from 'stream';
|
||||
import os from 'os';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('debug:wait', () => {
|
||||
const action = createWaitAction();
|
||||
@@ -27,25 +26,23 @@ describe('debug:wait', () => {
|
||||
write: jest.fn(),
|
||||
} as jest.Mocked<Partial<Writable>> as jest.Mocked<Writable>;
|
||||
|
||||
const mockTmpDir = os.tmpdir();
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = mockDir.resolve('workspace');
|
||||
|
||||
const mockContext = {
|
||||
input: {},
|
||||
baseUrl: 'somebase',
|
||||
workspacePath: mockTmpDir,
|
||||
workspacePath,
|
||||
logger: getVoidLogger(),
|
||||
logStream,
|
||||
output: jest.fn(),
|
||||
createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir),
|
||||
createTemporaryDirectory: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should wait for specified period of time', async () => {
|
||||
const context = {
|
||||
...mockContext,
|
||||
|
||||
+21
-42
@@ -14,10 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import os from 'os';
|
||||
import { join as joinPath, sep as pathSep } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import {
|
||||
getVoidLogger,
|
||||
resolvePackagePath,
|
||||
@@ -33,6 +31,7 @@ import {
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
import { examples } from './template.examples';
|
||||
import yaml from 'yaml';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
jest.mock('@backstage/plugin-scaffolder-node', () => ({
|
||||
...jest.requireActual('@backstage/plugin-scaffolder-node'),
|
||||
@@ -45,16 +44,6 @@ type FetchTemplateInput = ReturnType<
|
||||
? U
|
||||
: never;
|
||||
|
||||
const realFiles = Object.fromEntries(
|
||||
[
|
||||
resolvePackagePath(
|
||||
'@backstage/plugin-scaffolder-backend',
|
||||
'assets',
|
||||
'nunjucks.js.txt',
|
||||
),
|
||||
].map(k => [k, mockFs.load(k)]),
|
||||
);
|
||||
|
||||
const aBinaryFile = fs.readFileSync(
|
||||
resolvePackagePath(
|
||||
'@backstage/plugin-scaffolder-backend',
|
||||
@@ -69,14 +58,8 @@ const mockFetchContents = fetchContents as jest.MockedFunction<
|
||||
describe('fetch:template examples', () => {
|
||||
let action: TemplateAction<any>;
|
||||
|
||||
const workspacePath = os.tmpdir();
|
||||
const createTemporaryDirectory: jest.MockedFunction<
|
||||
ActionContext<FetchTemplateInput>['createTemporaryDirectory']
|
||||
> = jest.fn(() =>
|
||||
Promise.resolve(
|
||||
joinPath(workspacePath, `${createTemporaryDirectory.mock.calls.length}`),
|
||||
),
|
||||
);
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = mockDir.resolve('workspace');
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
@@ -90,24 +73,20 @@ describe('fetch:template examples', () => {
|
||||
logStream: new PassThrough(),
|
||||
logger,
|
||||
workspacePath,
|
||||
createTemporaryDirectory,
|
||||
|
||||
async createTemporaryDirectory() {
|
||||
return fs.mkdtemp(mockDir.resolve('tmp-'));
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
});
|
||||
|
||||
mockDir.clear();
|
||||
action = createFetchTemplateAction({
|
||||
reader: Symbol('UrlReader') as unknown as UrlReader,
|
||||
integrations: Symbol('Integrations') as unknown as ScmIntegrations,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
describe('handler', () => {
|
||||
describe('with valid input', () => {
|
||||
let context: ActionContext<FetchTemplateInput>;
|
||||
@@ -116,13 +95,13 @@ describe('fetch:template examples', () => {
|
||||
context = mockContext(yaml.parse(examples[0].example).steps[0].input);
|
||||
|
||||
mockFetchContents.mockImplementation(({ outputPath }) => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
mockDir.setContent({
|
||||
[outputPath]: {
|
||||
'an-executable.sh': mockFs.file({
|
||||
content: '#!/usr/bin/env bash',
|
||||
mode: parseInt('100755', 8),
|
||||
}),
|
||||
'an-executable.sh': ctx =>
|
||||
fs.writeFileSync(ctx.path, '#!/usr/bin/env bash', {
|
||||
encoding: 'utf8',
|
||||
mode: parseInt('100755', 8),
|
||||
}),
|
||||
'empty-dir-${{ values.count }}': {},
|
||||
'static.txt': 'static content',
|
||||
'${{ values.name }}.txt': 'static content',
|
||||
@@ -132,12 +111,8 @@ describe('fetch:template examples', () => {
|
||||
},
|
||||
'.${{ values.name }}': '${{ values.itemList | dump }}',
|
||||
'a-binary-file.png': aBinaryFile,
|
||||
symlink: mockFs.symlink({
|
||||
path: 'a-binary-file.png',
|
||||
}),
|
||||
brokenSymlink: mockFs.symlink({
|
||||
path: './not-a-real-file.txt',
|
||||
}),
|
||||
symlink: ctx => ctx.symlink('a-binary-file.png'),
|
||||
brokenSymlink: ctx => ctx.symlink('./not-a-real-file.txt'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -212,7 +187,11 @@ describe('fetch:template examples', () => {
|
||||
|
||||
await expect(
|
||||
fs.realpath(`${workspacePath}/target/symlink`),
|
||||
).resolves.toBe(joinPath(workspacePath, 'target', 'a-binary-file.png'));
|
||||
).resolves.toBe(
|
||||
fs.realpathSync(
|
||||
joinPath(workspacePath, 'target', 'a-binary-file.png'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('copies broken symlinks as-is without processing them', async () => {
|
||||
|
||||
@@ -19,10 +19,8 @@ jest.mock('@backstage/plugin-scaffolder-node', () => {
|
||||
return { ...actual, fetchContents: jest.fn() };
|
||||
});
|
||||
|
||||
import os from 'os';
|
||||
import { join as joinPath, sep as pathSep } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import {
|
||||
getVoidLogger,
|
||||
resolvePackagePath,
|
||||
@@ -36,6 +34,7 @@ import {
|
||||
ActionContext,
|
||||
TemplateAction,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
type FetchTemplateInput = ReturnType<
|
||||
typeof createFetchTemplateAction
|
||||
@@ -43,16 +42,6 @@ type FetchTemplateInput = ReturnType<
|
||||
? U
|
||||
: never;
|
||||
|
||||
const realFiles = Object.fromEntries(
|
||||
[
|
||||
resolvePackagePath(
|
||||
'@backstage/plugin-scaffolder-backend',
|
||||
'assets',
|
||||
'nunjucks.js.txt',
|
||||
),
|
||||
].map(k => [k, mockFs.load(k)]),
|
||||
);
|
||||
|
||||
const aBinaryFile = fs.readFileSync(
|
||||
resolvePackagePath(
|
||||
'@backstage/plugin-scaffolder-backend',
|
||||
@@ -67,14 +56,8 @@ const mockFetchContents = fetchContents as jest.MockedFunction<
|
||||
describe('fetch:template', () => {
|
||||
let action: TemplateAction<any>;
|
||||
|
||||
const workspacePath = os.tmpdir();
|
||||
const createTemporaryDirectory: jest.MockedFunction<
|
||||
ActionContext<FetchTemplateInput>['createTemporaryDirectory']
|
||||
> = jest.fn(() =>
|
||||
Promise.resolve(
|
||||
joinPath(workspacePath, `${createTemporaryDirectory.mock.calls.length}`),
|
||||
),
|
||||
);
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = mockDir.resolve('workspace');
|
||||
|
||||
const logger = getVoidLogger();
|
||||
|
||||
@@ -95,24 +78,21 @@ describe('fetch:template', () => {
|
||||
logStream: new PassThrough(),
|
||||
logger,
|
||||
workspacePath,
|
||||
createTemporaryDirectory,
|
||||
async createTemporaryDirectory() {
|
||||
return fs.mkdtemp(mockDir.resolve('tmp-'));
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
mockDir.setContent({
|
||||
workspace: {},
|
||||
});
|
||||
|
||||
action = createFetchTemplateAction({
|
||||
reader: Symbol('UrlReader') as unknown as UrlReader,
|
||||
integrations: Symbol('Integrations') as unknown as ScmIntegrations,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it(`returns a TemplateAction with the id 'fetch:template'`, () => {
|
||||
expect(action.id).toEqual('fetch:template');
|
||||
});
|
||||
@@ -190,8 +170,7 @@ describe('fetch:template', () => {
|
||||
});
|
||||
|
||||
mockFetchContents.mockImplementation(({ outputPath }) => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
mockDir.setContent({
|
||||
[outputPath]: {
|
||||
'{% if values.showDummyFile %}dummy-file.txt{% else %}{% endif %}':
|
||||
'dummy file',
|
||||
@@ -282,13 +261,8 @@ describe('fetch:template', () => {
|
||||
});
|
||||
|
||||
mockFetchContents.mockImplementation(({ outputPath }) => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
mockDir.setContent({
|
||||
[outputPath]: {
|
||||
'an-executable.sh': mockFs.file({
|
||||
content: '#!/usr/bin/env bash',
|
||||
mode: parseInt('100755', 8),
|
||||
}),
|
||||
'empty-dir-${{ values.count }}': {},
|
||||
'static.txt': 'static content',
|
||||
'${{ values.name }}.txt': 'static content',
|
||||
@@ -298,12 +272,13 @@ describe('fetch:template', () => {
|
||||
},
|
||||
'.${{ values.name }}': '${{ values.itemList | dump }}',
|
||||
'a-binary-file.png': aBinaryFile,
|
||||
symlink: mockFs.symlink({
|
||||
path: 'a-binary-file.png',
|
||||
}),
|
||||
brokenSymlink: mockFs.symlink({
|
||||
path: './not-a-real-file.txt',
|
||||
}),
|
||||
'an-executable.sh': ctx =>
|
||||
fs.writeFileSync(ctx.path, '#!/usr/bin/env bash', {
|
||||
encoding: 'utf-8',
|
||||
mode: parseInt('100755', 8),
|
||||
}),
|
||||
symlink: ctx => ctx.symlink('a-binary-file.png'),
|
||||
brokenSymlink: ctx => ctx.symlink('./not-a-real-file.txt'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -378,7 +353,11 @@ describe('fetch:template', () => {
|
||||
|
||||
await expect(
|
||||
fs.realpath(`${workspacePath}/target/symlink`),
|
||||
).resolves.toBe(joinPath(workspacePath, 'target', 'a-binary-file.png'));
|
||||
).resolves.toBe(
|
||||
fs.realpathSync(
|
||||
joinPath(workspacePath, 'target', 'a-binary-file.png'),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('copies broken symlinks as-is without processing them', async () => {
|
||||
@@ -408,8 +387,7 @@ describe('fetch:template', () => {
|
||||
});
|
||||
|
||||
mockFetchContents.mockImplementation(({ outputPath }) => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
mockDir.setContent({
|
||||
[outputPath]: {
|
||||
processed: {
|
||||
'templated-content-${{ values.name }}.txt': '${{ values.count }}',
|
||||
@@ -458,8 +436,7 @@ describe('fetch:template', () => {
|
||||
});
|
||||
|
||||
mockFetchContents.mockImplementation(({ outputPath }) => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
mockDir.setContent({
|
||||
[outputPath]: {
|
||||
processed: {
|
||||
'templated-content-${{ values.name }}.txt': '${{ values.count }}',
|
||||
@@ -509,8 +486,7 @@ describe('fetch:template', () => {
|
||||
});
|
||||
|
||||
mockFetchContents.mockImplementation(({ outputPath }) => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
mockDir.setContent({
|
||||
[outputPath]: {
|
||||
'{{ cookiecutter.name }}.txt': 'static content',
|
||||
subdir: {
|
||||
@@ -564,8 +540,7 @@ describe('fetch:template', () => {
|
||||
});
|
||||
|
||||
mockFetchContents.mockImplementation(({ outputPath }) => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
mockDir.setContent({
|
||||
[outputPath]: {
|
||||
'empty-dir-${{ values.count }}': {},
|
||||
'static.txt': 'static content',
|
||||
@@ -646,8 +621,7 @@ describe('fetch:template', () => {
|
||||
});
|
||||
|
||||
mockFetchContents.mockImplementation(({ outputPath }) => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
mockDir.setContent({
|
||||
[outputPath]: {
|
||||
'${{ values.name }}.njk': '${{ values.name }}: ${{ values.count }}',
|
||||
'${{ values.name }}.txt.jinja2':
|
||||
@@ -687,8 +661,7 @@ describe('fetch:template', () => {
|
||||
});
|
||||
|
||||
mockFetchContents.mockImplementation(({ outputPath }) => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
mockDir.setContent({
|
||||
[joinPath(workspacePath, 'target')]: {
|
||||
'static-content.txt': 'static-content',
|
||||
},
|
||||
@@ -703,10 +676,6 @@ describe('fetch:template', () => {
|
||||
await action.handler(context);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('overwrites existing file', async () => {
|
||||
await expect(
|
||||
fs.readFile(`${workspacePath}/target/static-content.txt`, 'utf-8'),
|
||||
@@ -728,8 +697,7 @@ describe('fetch:template', () => {
|
||||
});
|
||||
|
||||
mockFetchContents.mockImplementation(({ outputPath }) => {
|
||||
mockFs({
|
||||
...realFiles,
|
||||
mockDir.setContent({
|
||||
[joinPath(workspacePath, 'target')]: {
|
||||
'static-content.txt': 'static-content',
|
||||
},
|
||||
|
||||
+5
-10
@@ -18,18 +18,17 @@ import { createFilesystemDeleteAction } from './delete';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { PassThrough } from 'stream';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import * as os from 'os';
|
||||
import mockFs from 'mock-fs';
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'yaml';
|
||||
import { examples } from './delete.examples';
|
||||
|
||||
const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
|
||||
const workspacePath = resolvePath(root, 'my-workspace');
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('fs:delete examples', () => {
|
||||
const action = createFilesystemDeleteAction();
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = resolvePath(mockDir.path, 'workspace');
|
||||
|
||||
const files: string[] = yaml.parse(examples[0].example).steps[0].input.files;
|
||||
|
||||
const mockContext = {
|
||||
@@ -46,7 +45,7 @@ describe('fs:delete examples', () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
[files[0]]: 'hello',
|
||||
[files[1]]: 'world',
|
||||
@@ -57,10 +56,6 @@ describe('fs:delete examples', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should call fs.rm with the correct values', async () => {
|
||||
files.forEach(file => {
|
||||
const filePath = resolvePath(workspacePath, file);
|
||||
|
||||
+5
-10
@@ -14,20 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as os from 'os';
|
||||
import mockFs from 'mock-fs';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { createFilesystemDeleteAction } from './delete';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { PassThrough } from 'stream';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
|
||||
const workspacePath = resolvePath(root, 'my-workspace');
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('fs:delete', () => {
|
||||
const action = createFilesystemDeleteAction();
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = resolvePath(mockDir.path, 'workspace');
|
||||
|
||||
const mockContext = {
|
||||
input: {
|
||||
files: ['unit-test-a.js', 'unit-test-b.js'],
|
||||
@@ -42,7 +41,7 @@ describe('fs:delete', () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
'unit-test-a.js': 'hello',
|
||||
'unit-test-b.js': 'world',
|
||||
@@ -53,10 +52,6 @@ describe('fs:delete', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should throw an error when files is not an array', async () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
|
||||
+5
-10
@@ -14,8 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as os from 'os';
|
||||
import mockFs from 'mock-fs';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { createFilesystemRenameAction } from './rename';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
@@ -23,15 +21,16 @@ import { PassThrough } from 'stream';
|
||||
import fs from 'fs-extra';
|
||||
import yaml from 'yaml';
|
||||
import { examples } from './rename.examples';
|
||||
|
||||
const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
|
||||
const workspacePath = resolvePath(root, 'my-workspace');
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('fs:rename examples', () => {
|
||||
const action = createFilesystemRenameAction();
|
||||
const files: { from: string; to: string }[] = yaml.parse(examples[0].example)
|
||||
.steps[0].input.files;
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = resolvePath(mockDir.path, 'workspace');
|
||||
|
||||
const mockContext = {
|
||||
input: {
|
||||
files: files,
|
||||
@@ -46,7 +45,7 @@ describe('fs:rename examples', () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
[files[0].from]: 'hello',
|
||||
[files[1].from]: 'world',
|
||||
@@ -59,10 +58,6 @@ describe('fs:rename examples', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should call fs.move with the correct values', async () => {
|
||||
mockContext.input.files.forEach(file => {
|
||||
const filePath = resolvePath(workspacePath, file.from);
|
||||
|
||||
+5
-10
@@ -14,20 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as os from 'os';
|
||||
import mockFs from 'mock-fs';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { createFilesystemRenameAction } from './rename';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { PassThrough } from 'stream';
|
||||
import fs from 'fs-extra';
|
||||
|
||||
const root = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
|
||||
const workspacePath = resolvePath(root, 'my-workspace');
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('fs:rename', () => {
|
||||
const action = createFilesystemRenameAction();
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = resolvePath(mockDir.path, 'workspace');
|
||||
|
||||
const mockInputFiles = [
|
||||
{
|
||||
from: 'unit-test-a.js',
|
||||
@@ -56,7 +55,7 @@ describe('fs:rename', () => {
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
'unit-test-a.js': 'hello',
|
||||
'unit-test-b.js': 'world',
|
||||
@@ -68,10 +67,6 @@ describe('fs:rename', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should throw an error when files is not an array', async () => {
|
||||
await expect(
|
||||
action.handler({
|
||||
|
||||
+28
-28
@@ -24,21 +24,17 @@ import {
|
||||
ActionContext,
|
||||
TemplateAction,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import { Writable } from 'stream';
|
||||
import {
|
||||
createPublishGithubPullRequestAction,
|
||||
OctokitWithPullRequestPluginClient,
|
||||
} from './githubPullRequest';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
// Make sure root logger is initialized ahead of FS mock
|
||||
createRootLogger();
|
||||
|
||||
const root = os.platform() === 'win32' ? 'C:\\root' : '/root';
|
||||
const workspacePath = resolvePath(root, 'my-workspace');
|
||||
|
||||
type GithubPullRequestActionInput = ReturnType<
|
||||
typeof createPublishGithubPullRequestAction
|
||||
> extends TemplateAction<infer U>
|
||||
@@ -54,7 +50,12 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
};
|
||||
};
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = mockDir.resolve('workspace');
|
||||
|
||||
beforeEach(() => {
|
||||
mockDir.clear();
|
||||
|
||||
const integrations = ScmIntegrations.fromConfig(new ConfigReader({}));
|
||||
fakeClient = {
|
||||
createPullRequest: jest.fn(async (_: any) => {
|
||||
@@ -92,7 +93,6 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
@@ -132,7 +132,7 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
draft: true,
|
||||
};
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: { 'file.txt': 'Hello there!' },
|
||||
});
|
||||
|
||||
@@ -197,7 +197,7 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
draft: true,
|
||||
};
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: { 'file.txt': 'Hello there!' },
|
||||
});
|
||||
|
||||
@@ -261,7 +261,7 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
sourcePath: 'source',
|
||||
};
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -323,7 +323,7 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
description: 'This PR is really good',
|
||||
};
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: { 'file.txt': 'Hello there!' },
|
||||
});
|
||||
|
||||
@@ -385,7 +385,7 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
teamReviewers: ['team-foo'],
|
||||
};
|
||||
|
||||
mockFs({ [workspacePath]: {} });
|
||||
mockDir.setContent({ [workspacePath]: {} });
|
||||
|
||||
ctx = {
|
||||
createTemporaryDirectory: jest.fn(),
|
||||
@@ -437,7 +437,7 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
description: 'This PR is really good',
|
||||
};
|
||||
|
||||
mockFs({ [workspacePath]: {} });
|
||||
mockDir.setContent({ [workspacePath]: {} });
|
||||
|
||||
ctx = {
|
||||
createTemporaryDirectory: jest.fn(),
|
||||
@@ -469,11 +469,9 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
description: 'This PR is really good',
|
||||
};
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
Makefile: mockFs.symlink({
|
||||
path: '../../nothing/yet',
|
||||
}),
|
||||
Makefile: c => c.symlink('../../nothing/yet'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -523,12 +521,13 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
description: 'This PR is really good',
|
||||
};
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
'hello.sh': mockFs.file({
|
||||
content: 'echo Hello there!',
|
||||
mode: 0o100755,
|
||||
}),
|
||||
'hello.sh': c =>
|
||||
fs.writeFileSync(c.path, 'echo Hello there!', {
|
||||
encoding: 'utf8',
|
||||
mode: 0o100755,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -588,12 +587,13 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
description: 'This PR is really good',
|
||||
};
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
'hello.sh': mockFs.file({
|
||||
content: 'echo Hello there!',
|
||||
mode: 0o100775,
|
||||
}),
|
||||
'hello.sh': c =>
|
||||
fs.writeFileSync(c.path, 'echo Hello there!', {
|
||||
encoding: 'utf8',
|
||||
mode: 0o100775,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -654,7 +654,7 @@ describe('createPublishGithubPullRequestAction', () => {
|
||||
commitMessage: 'Create my new app, but in the commit message',
|
||||
};
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: { 'file.txt': 'Hello there!' },
|
||||
});
|
||||
|
||||
|
||||
+21
-25
@@ -17,18 +17,13 @@ import { createRootLogger, getRootLogger } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import { Writable } from 'stream';
|
||||
import { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
// Make sure root logger is initialized ahead of FS mock
|
||||
createRootLogger();
|
||||
|
||||
const root = os.platform() === 'win32' ? 'C:\\root' : '/root';
|
||||
const workspacePath = resolvePath(root, 'my-workspace');
|
||||
|
||||
const mockGitlabClient = {
|
||||
Namespaces: {
|
||||
show: jest.fn(),
|
||||
@@ -79,7 +74,12 @@ jest.mock('@gitbeaker/node', () => ({
|
||||
describe('createGitLabMergeRequest', () => {
|
||||
let instance: TemplateAction<any>;
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
const workspacePath = mockDir.resolve('workspace');
|
||||
|
||||
beforeEach(() => {
|
||||
mockDir.clear();
|
||||
|
||||
const config = new ConfigReader({
|
||||
integrations: {
|
||||
gitlab: [
|
||||
@@ -100,10 +100,6 @@ describe('createGitLabMergeRequest', () => {
|
||||
instance = createPublishGitlabMergeRequestAction({ integrations });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
describe('createGitLabMergeRequestWithSpecifiedTargetBranch', () => {
|
||||
it('removeSourceBranch is false by default when not passed in options', async () => {
|
||||
const input = {
|
||||
@@ -114,7 +110,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
description: 'This MR is really good',
|
||||
targetPath: 'Subdirectory',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -156,7 +152,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
description: 'This MR is really good',
|
||||
targetPath: 'Subdirectory',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -200,7 +196,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
removeSourceBranch: true,
|
||||
targetPath: 'Subdirectory',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -235,7 +231,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
removeSourceBranch: false,
|
||||
targetPath: 'Subdirectory',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -276,7 +272,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
targetPath: 'Subdirectory',
|
||||
assignee: 'John Smith',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -316,7 +312,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
targetPath: 'Subdirectory',
|
||||
assingnee: 'John Doe',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -356,7 +352,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
removeSourceBranch: false,
|
||||
targetPath: 'Subdirectory',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -395,7 +391,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
targetPath: 'Subdirectory',
|
||||
assignee: 'Unknown',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -431,7 +427,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
branchName: 'new-mr',
|
||||
description: 'This MR is really good',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -480,7 +476,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
description: 'This MR is really good',
|
||||
targetPath: 'source',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -523,7 +519,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
commitAction: 'create',
|
||||
targetPath: 'source',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -565,7 +561,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
commitAction: 'update',
|
||||
targetPath: 'source',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -607,7 +603,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
commitAction: 'delete',
|
||||
targetPath: 'source',
|
||||
};
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -652,7 +648,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
commitAction: 'create',
|
||||
};
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
@@ -697,7 +693,7 @@ describe('createGitLabMergeRequest', () => {
|
||||
commitAction: 'create',
|
||||
};
|
||||
|
||||
mockFs({
|
||||
mockDir.setContent({
|
||||
[workspacePath]: {
|
||||
source: { 'foo.txt': 'Hello there!' },
|
||||
irrelevant: { 'bar.txt': 'Nothing to see here' },
|
||||
|
||||
@@ -14,10 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import mockFs from 'mock-fs';
|
||||
import * as winston from 'winston';
|
||||
|
||||
import { getVoidLogger, resolvePackagePath } from '@backstage/backend-common';
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner';
|
||||
import { TemplateActionRegistry } from '../actions';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
@@ -36,19 +33,7 @@ import {
|
||||
PermissionEvaluator,
|
||||
} from '@backstage/plugin-permission-common';
|
||||
import { RESOURCE_TYPE_SCAFFOLDER_ACTION } from '@backstage/plugin-scaffolder-common/alpha';
|
||||
|
||||
// The Stream module is lazy loaded, so make sure it's in the module cache before mocking fs
|
||||
void winston.transports.Stream;
|
||||
|
||||
const realFiles = Object.fromEntries(
|
||||
[
|
||||
resolvePackagePath(
|
||||
'@backstage/plugin-scaffolder-backend',
|
||||
'assets',
|
||||
'nunjucks.js.txt',
|
||||
),
|
||||
].map(k => [k, mockFs.load(k)]),
|
||||
);
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
|
||||
describe('DefaultWorkflowRunner', () => {
|
||||
const logger = getVoidLogger();
|
||||
@@ -56,6 +41,8 @@ describe('DefaultWorkflowRunner', () => {
|
||||
let runner: NunjucksWorkflowRunner;
|
||||
let fakeActionHandler: jest.Mock;
|
||||
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
const mockedPermissionApi: jest.Mocked<PermissionEvaluator> = {
|
||||
authorizeConditional: jest.fn(),
|
||||
} as unknown as jest.Mocked<PermissionEvaluator>;
|
||||
@@ -84,11 +71,7 @@ describe('DefaultWorkflowRunner', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
winston.format.simple(); // put logform in the require.cache before mocking fs
|
||||
mockFs({
|
||||
'/tmp': mockFs.directory(),
|
||||
...realFiles,
|
||||
});
|
||||
mockDir.clear();
|
||||
|
||||
jest.resetAllMocks();
|
||||
actionRegistry = new TemplateActionRegistry();
|
||||
@@ -148,16 +131,12 @@ describe('DefaultWorkflowRunner', () => {
|
||||
runner = new NunjucksWorkflowRunner({
|
||||
actionRegistry,
|
||||
integrations,
|
||||
workingDirectory: '/tmp',
|
||||
workingDirectory: mockDir.path,
|
||||
logger,
|
||||
permissions: mockedPermissionApi,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it('should throw an error if the action does not exist', async () => {
|
||||
const task = createMockTaskWithSpec({
|
||||
apiVersion: 'scaffolder.backstage.io/v1beta3',
|
||||
|
||||
@@ -48,7 +48,10 @@ export type SearchResultItemExtensionOptions<
|
||||
},
|
||||
> = {
|
||||
id: string;
|
||||
at?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
component: (options: {
|
||||
config: TConfig;
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('createSearchResultListItemExtension', () => {
|
||||
const TechDocsSearchResultItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
id: 'techdocs',
|
||||
at: 'plugin.search.page/items',
|
||||
attachTo: { id: 'plugin.search.page', input: 'items' },
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
noTrack: z.boolean().default(true),
|
||||
@@ -79,7 +79,7 @@ describe('createSearchResultListItemExtension', () => {
|
||||
const ExploreSearchResultItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
id: 'explore',
|
||||
at: 'plugin.search.page/items',
|
||||
attachTo: { id: 'plugin.search.page', input: 'items' },
|
||||
predicate: result => result.type === 'explore',
|
||||
component: async () => ExploreSearchResultItemComponent,
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ export type SearchResultItemExtensionOptions<
|
||||
/**
|
||||
* The extension attachment point (e.g., search modal or page).
|
||||
*/
|
||||
at?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
/**
|
||||
* Optional extension config schema.
|
||||
*/
|
||||
@@ -97,7 +97,7 @@ export function createSearchResultListItemExtension<
|
||||
) as PortableSchema<TConfig>);
|
||||
return createExtension({
|
||||
id: `plugin.search.result.item.${options.id}`,
|
||||
at: options.at ?? 'plugin.search.page/items',
|
||||
attachTo: options.attachTo ?? { id: 'plugin.search.page', input: 'items' },
|
||||
configSchema,
|
||||
output: {
|
||||
item: searchResultItemExtensionData,
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import {
|
||||
Entity,
|
||||
DEFAULT_NAMESPACE,
|
||||
Entity,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import { Config } from '@backstage/config';
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
PreparerBase,
|
||||
PreparerBuilder,
|
||||
PublisherBase,
|
||||
UrlPreparer,
|
||||
} from '@backstage/plugin-techdocs-node';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
@@ -194,7 +193,7 @@ export class DocsBuilder {
|
||||
// Remove Prepared directory since it is no longer needed.
|
||||
// Caveat: Can not remove prepared directory in case of git preparer since the
|
||||
// local git repository is used to get etag on subsequent requests.
|
||||
if (this.preparer instanceof UrlPreparer) {
|
||||
if (this.preparer.shouldCleanPreparedDirectory()) {
|
||||
this.logger.debug(
|
||||
`Removing prepared directory ${preparedDir} since the site has been generated`,
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Writable } from 'stream';
|
||||
export class DirectoryPreparer implements PreparerBase {
|
||||
static fromConfig(config: Config, options: PreparerConfig): DirectoryPreparer;
|
||||
prepare(entity: Entity, options?: PreparerOptions): Promise<PreparerResponse>;
|
||||
shouldCleanPreparedDirectory(): boolean;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -132,6 +133,7 @@ export const parseReferenceAnnotation: (
|
||||
// @public
|
||||
export type PreparerBase = {
|
||||
prepare(entity: Entity, options?: PreparerOptions): Promise<PreparerResponse>;
|
||||
shouldCleanPreparedDirectory(): boolean;
|
||||
};
|
||||
|
||||
// @public
|
||||
@@ -274,5 +276,6 @@ export const transformDirLocation: (
|
||||
export class UrlPreparer implements PreparerBase {
|
||||
static fromConfig(options: PreparerConfig): UrlPreparer;
|
||||
prepare(entity: Entity, options?: PreparerOptions): Promise<PreparerResponse>;
|
||||
shouldCleanPreparedDirectory(): boolean;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -63,17 +63,16 @@
|
||||
"js-yaml": "^4.0.0",
|
||||
"json5": "^2.1.3",
|
||||
"mime-types": "^2.1.27",
|
||||
"mock-fs": "^5.2.0",
|
||||
"p-limit": "^3.1.0",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"winston": "^3.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/backend-test-utils": "workspace:^",
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@types/fs-extra": "^9.0.5",
|
||||
"@types/js-yaml": "^4.0.0",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/mock-fs": "^4.13.0",
|
||||
"@types/recursive-readdir": "^2.2.0",
|
||||
"@types/supertest": "^2.0.8",
|
||||
"aws-sdk-client-mock": "^2.0.0",
|
||||
|
||||
@@ -17,9 +17,8 @@
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { createMockDirectory } from '@backstage/backend-test-utils';
|
||||
import fs from 'fs-extra';
|
||||
import mockFs from 'mock-fs';
|
||||
import os from 'os';
|
||||
import path, { resolve as resolvePath } from 'path';
|
||||
import { ParsedLocationAnnotation } from '../../helpers';
|
||||
import {
|
||||
@@ -88,14 +87,12 @@ const mkdocsYmlWithEnvTag = fs.readFileSync(
|
||||
const mockLogger = getVoidLogger();
|
||||
const warn = jest.spyOn(mockLogger, 'warn');
|
||||
|
||||
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
|
||||
|
||||
const scmIntegrations = ScmIntegrations.fromConfig(new ConfigReader({}));
|
||||
|
||||
describe('helpers', () => {
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
const mockDir = createMockDirectory();
|
||||
|
||||
afterEach(mockDir.clear);
|
||||
|
||||
describe('getGeneratorKey', () => {
|
||||
it('should return techdocs as the only generator key', () => {
|
||||
@@ -188,13 +185,13 @@ describe('helpers', () => {
|
||||
|
||||
describe('patchMkdocsYmlPreBuild', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'/mkdocs.yml': mkdocsYml,
|
||||
'/mkdocs_default.yml': mkdocsDefaultYml,
|
||||
'/mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl,
|
||||
'/mkdocs_with_edit_uri.yml': mkdocsYmlWithEditUri,
|
||||
'/mkdocs_with_extensions.yml': mkdocsYmlWithExtensions,
|
||||
'/mkdocs_with_comments.yml': mkdocsYmlWithComments,
|
||||
mockDir.setContent({
|
||||
'mkdocs.yml': mkdocsYml,
|
||||
'mkdocs_default.yml': mkdocsDefaultYml,
|
||||
'mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl,
|
||||
'mkdocs_with_edit_uri.yml': mkdocsYmlWithEditUri,
|
||||
'mkdocs_with_extensions.yml': mkdocsYmlWithExtensions,
|
||||
'mkdocs_with_comments.yml': mkdocsYmlWithComments,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -205,13 +202,13 @@ describe('helpers', () => {
|
||||
};
|
||||
|
||||
await patchMkdocsYmlPreBuild(
|
||||
'/mkdocs.yml',
|
||||
mockDir.resolve('mkdocs.yml'),
|
||||
mockLogger,
|
||||
parsedLocationAnnotation,
|
||||
scmIntegrations,
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile('/mkdocs.yml');
|
||||
const updatedMkdocsYml = await fs.readFile(mockDir.resolve('mkdocs.yml'));
|
||||
|
||||
expect(updatedMkdocsYml.toString()).toContain(
|
||||
'repo_url: https://github.com/backstage/backstage',
|
||||
@@ -225,13 +222,15 @@ describe('helpers', () => {
|
||||
};
|
||||
|
||||
await patchMkdocsYmlPreBuild(
|
||||
'/mkdocs_with_extensions.yml',
|
||||
mockDir.resolve('mkdocs_with_extensions.yml'),
|
||||
mockLogger,
|
||||
parsedLocationAnnotation,
|
||||
scmIntegrations,
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile('/mkdocs_with_extensions.yml');
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
mockDir.resolve('mkdocs_with_extensions.yml'),
|
||||
);
|
||||
|
||||
expect(updatedMkdocsYml.toString()).toContain(
|
||||
'repo_url: https://github.com/backstage/backstage',
|
||||
@@ -248,13 +247,15 @@ describe('helpers', () => {
|
||||
};
|
||||
|
||||
await patchMkdocsYmlPreBuild(
|
||||
'/mkdocs_with_repo_url.yml',
|
||||
mockDir.resolve('mkdocs_with_repo_url.yml'),
|
||||
mockLogger,
|
||||
parsedLocationAnnotation,
|
||||
scmIntegrations,
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile('/mkdocs_with_repo_url.yml');
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
mockDir.resolve('mkdocs_with_repo_url.yml'),
|
||||
);
|
||||
|
||||
expect(updatedMkdocsYml.toString()).toContain(
|
||||
'repo_url: https://github.com/backstage/backstage',
|
||||
@@ -271,13 +272,15 @@ describe('helpers', () => {
|
||||
};
|
||||
|
||||
await patchMkdocsYmlPreBuild(
|
||||
'/mkdocs_with_edit_uri.yml',
|
||||
mockDir.resolve('mkdocs_with_edit_uri.yml'),
|
||||
mockLogger,
|
||||
parsedLocationAnnotation,
|
||||
scmIntegrations,
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile('/mkdocs_with_edit_uri.yml');
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
mockDir.resolve('mkdocs_with_edit_uri.yml'),
|
||||
);
|
||||
|
||||
expect(updatedMkdocsYml.toString()).toContain(
|
||||
'edit_uri: https://github.com/backstage/backstage/edit/main/docs',
|
||||
@@ -294,13 +297,15 @@ describe('helpers', () => {
|
||||
};
|
||||
|
||||
await patchMkdocsYmlPreBuild(
|
||||
'/mkdocs_with_comments.yml',
|
||||
mockDir.resolve('mkdocs_with_comments.yml'),
|
||||
mockLogger,
|
||||
parsedLocationAnnotation,
|
||||
scmIntegrations,
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile('/mkdocs_with_comments.yml');
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
mockDir.resolve('mkdocs_with_comments.yml'),
|
||||
);
|
||||
|
||||
expect(updatedMkdocsYml.toString()).toContain(
|
||||
'# This is a comment that is removed after editing',
|
||||
@@ -312,20 +317,20 @@ describe('helpers', () => {
|
||||
|
||||
describe('patchMkdocsYmlWithPlugins', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
'/mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins,
|
||||
'/mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins,
|
||||
'/mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins,
|
||||
mockDir.setContent({
|
||||
'mkdocs_with_techdocs_plugin.yml': mkdocsYmlWithTechdocsPlugins,
|
||||
'mkdocs_without_plugins.yml': mkdocsYmlWithoutPlugins,
|
||||
'mkdocs_with_additional_plugins.yml': mkdocsYmlWithAdditionalPlugins,
|
||||
});
|
||||
});
|
||||
it('should not add additional plugins if techdocs exists already in mkdocs file', async () => {
|
||||
await patchMkdocsYmlWithPlugins(
|
||||
'/mkdocs_with_techdocs_plugin.yml',
|
||||
mockDir.resolve('mkdocs_with_techdocs_plugin.yml'),
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
'/mkdocs_with_techdocs_plugin.yml',
|
||||
mockDir.resolve('mkdocs_with_techdocs_plugin.yml'),
|
||||
);
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
|
||||
plugins: string[];
|
||||
@@ -335,11 +340,13 @@ describe('helpers', () => {
|
||||
});
|
||||
it("should add the needed plugin if it doesn't exist in mkdocs file", async () => {
|
||||
await patchMkdocsYmlWithPlugins(
|
||||
'/mkdocs_without_plugins.yml',
|
||||
mockDir.resolve('mkdocs_without_plugins.yml'),
|
||||
mockLogger,
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile('/mkdocs_without_plugins.yml');
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
mockDir.resolve('mkdocs_without_plugins.yml'),
|
||||
);
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
|
||||
plugins: string[];
|
||||
};
|
||||
@@ -348,11 +355,11 @@ describe('helpers', () => {
|
||||
});
|
||||
it('should not override existing plugins', async () => {
|
||||
await patchMkdocsYmlWithPlugins(
|
||||
'/mkdocs_with_additional_plugins.yml',
|
||||
mockDir.resolve('mkdocs_with_additional_plugins.yml'),
|
||||
mockLogger,
|
||||
);
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
'/mkdocs_with_additional_plugins.yml',
|
||||
mockDir.resolve('mkdocs_with_additional_plugins.yml'),
|
||||
);
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
|
||||
plugins: string[];
|
||||
@@ -364,13 +371,13 @@ describe('helpers', () => {
|
||||
});
|
||||
it('should add all provided default plugins', async () => {
|
||||
await patchMkdocsYmlWithPlugins(
|
||||
'/mkdocs_with_additional_plugins.yml',
|
||||
mockDir.resolve('mkdocs_with_additional_plugins.yml'),
|
||||
mockLogger,
|
||||
['techdocs-core', 'custom-plugin'],
|
||||
);
|
||||
|
||||
const updatedMkdocsYml = await fs.readFile(
|
||||
'/mkdocs_with_additional_plugins.yml',
|
||||
mockDir.resolve('mkdocs_with_additional_plugins.yml'),
|
||||
);
|
||||
const parsedYml = yaml.load(updatedMkdocsYml.toString()) as {
|
||||
plugins: string[];
|
||||
@@ -386,45 +393,45 @@ describe('helpers', () => {
|
||||
warn.mockClear();
|
||||
});
|
||||
it('should have no effect if docs/index.md exists', async () => {
|
||||
mockFs({
|
||||
'/docs/index.md': 'index.md content',
|
||||
'/docs/README.md': 'docs/README.md content',
|
||||
mockDir.setContent({
|
||||
'docs/index.md': 'index.md content',
|
||||
'docs/README.md': 'docs/README.md content',
|
||||
});
|
||||
|
||||
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
|
||||
await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger });
|
||||
|
||||
await expect(fs.readFile('/docs/index.md', 'utf-8')).resolves.toEqual(
|
||||
'index.md content',
|
||||
);
|
||||
await expect(
|
||||
fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'),
|
||||
).resolves.toEqual('index.md content');
|
||||
expect(warn).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it("should use docs/README.md if docs/index.md doesn't exists", async () => {
|
||||
mockFs({
|
||||
'/docs/README.md': 'docs/README.md content',
|
||||
'/README.md': 'main README.md content',
|
||||
mockDir.setContent({
|
||||
'docs/README.md': 'docs/README.md content',
|
||||
'README.md': 'main README.md content',
|
||||
});
|
||||
|
||||
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
|
||||
await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger });
|
||||
|
||||
await expect(fs.readFile('/docs/index.md', 'utf-8')).resolves.toEqual(
|
||||
'docs/README.md content',
|
||||
);
|
||||
await expect(
|
||||
fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'),
|
||||
).resolves.toEqual('docs/README.md content');
|
||||
expect(warn.mock.calls).toEqual([
|
||||
[`${path.normalize('docs/index.md')} not found.`],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use README.md if neither docs/index.md or docs/README.md exist', async () => {
|
||||
mockFs({
|
||||
'/README.md': 'main README.md content',
|
||||
mockDir.setContent({
|
||||
'README.md': 'main README.md content',
|
||||
});
|
||||
|
||||
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
|
||||
await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger });
|
||||
|
||||
await expect(fs.readFile('/docs/index.md', 'utf-8')).resolves.toEqual(
|
||||
'main README.md content',
|
||||
);
|
||||
await expect(
|
||||
fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'),
|
||||
).resolves.toEqual('main README.md content');
|
||||
expect(warn.mock.calls).toEqual([
|
||||
[`${path.normalize('docs/index.md')} not found.`],
|
||||
[`${path.normalize('docs/README.md')} not found.`],
|
||||
@@ -433,11 +440,13 @@ describe('helpers', () => {
|
||||
});
|
||||
|
||||
it('should not use any file as index.md if no one matches the requirements', async () => {
|
||||
mockFs({});
|
||||
mockDir.setContent({});
|
||||
|
||||
await patchIndexPreBuild({ inputDir: '/', logger: mockLogger });
|
||||
await patchIndexPreBuild({ inputDir: mockDir.path, logger: mockLogger });
|
||||
|
||||
await expect(fs.readFile('/docs/index.md', 'utf-8')).rejects.toThrow();
|
||||
await expect(
|
||||
fs.readFile(mockDir.resolve('docs/index.md'), 'utf-8'),
|
||||
).rejects.toThrow();
|
||||
const paths = [
|
||||
path.normalize('docs/index.md'),
|
||||
path.normalize('docs/README.md'),
|
||||
@@ -449,7 +458,7 @@ describe('helpers', () => {
|
||||
...paths.map(p => [`${p} not found.`]),
|
||||
[
|
||||
`Could not find any techdocs' index file. Please make sure at least one of ${paths
|
||||
.map(p => path.sep + p)
|
||||
.map(p => mockDir.resolve(p))
|
||||
.join(' ')} exists.`,
|
||||
],
|
||||
]);
|
||||
@@ -463,13 +472,11 @@ describe('helpers', () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[rootDir]: mockFiles,
|
||||
});
|
||||
mockDir.setContent(mockFiles);
|
||||
});
|
||||
|
||||
it('should create the file if it does not exist', async () => {
|
||||
const filePath = path.join(rootDir, 'wrong_techdocs_metadata.json');
|
||||
const filePath = mockDir.resolve('wrong_techdocs_metadata.json');
|
||||
await createOrUpdateMetadata(filePath, mockLogger);
|
||||
|
||||
// Check if the file exists
|
||||
@@ -479,7 +486,7 @@ describe('helpers', () => {
|
||||
});
|
||||
|
||||
it('should throw error when the JSON is invalid', async () => {
|
||||
const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json');
|
||||
const filePath = mockDir.resolve('invalid_techdocs_metadata.json');
|
||||
|
||||
await expect(
|
||||
createOrUpdateMetadata(filePath, mockLogger),
|
||||
@@ -487,7 +494,7 @@ describe('helpers', () => {
|
||||
});
|
||||
|
||||
it('should add build timestamp to the metadata json', async () => {
|
||||
const filePath = path.join(rootDir, 'techdocs_metadata.json');
|
||||
const filePath = mockDir.resolve('techdocs_metadata.json');
|
||||
|
||||
await createOrUpdateMetadata(filePath, mockLogger);
|
||||
|
||||
@@ -496,7 +503,7 @@ describe('helpers', () => {
|
||||
});
|
||||
|
||||
it('should add list of files to the metadata json', async () => {
|
||||
const filePath = path.join(rootDir, 'techdocs_metadata.json');
|
||||
const filePath = mockDir.resolve('techdocs_metadata.json');
|
||||
|
||||
await createOrUpdateMetadata(filePath, mockLogger);
|
||||
|
||||
@@ -508,16 +515,14 @@ describe('helpers', () => {
|
||||
|
||||
describe('storeEtagMetadata', () => {
|
||||
beforeEach(() => {
|
||||
mockFs({
|
||||
[rootDir]: {
|
||||
'invalid_techdocs_metadata.json': 'dsds',
|
||||
'techdocs_metadata.json': '{"site_name": "Tech Docs"}',
|
||||
},
|
||||
mockDir.setContent({
|
||||
'invalid_techdocs_metadata.json': 'dsds',
|
||||
'techdocs_metadata.json': '{"site_name": "Tech Docs"}',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw error when the JSON is invalid', async () => {
|
||||
const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json');
|
||||
const filePath = mockDir.resolve('invalid_techdocs_metadata.json');
|
||||
|
||||
await expect(storeEtagMetadata(filePath, 'etag123abc')).rejects.toThrow(
|
||||
'Unexpected token',
|
||||
@@ -525,7 +530,7 @@ describe('helpers', () => {
|
||||
});
|
||||
|
||||
it('should add etag to the metadata json', async () => {
|
||||
const filePath = path.join(rootDir, 'techdocs_metadata.json');
|
||||
const filePath = mockDir.resolve('techdocs_metadata.json');
|
||||
|
||||
await storeEtagMetadata(filePath, 'etag123abc');
|
||||
|
||||
@@ -535,34 +540,31 @@ describe('helpers', () => {
|
||||
});
|
||||
|
||||
describe('getMkdocsYml', () => {
|
||||
const inputDir = resolvePath(__filename, '../__fixtures__/');
|
||||
const siteOptions = {
|
||||
name: mockEntity.metadata.title,
|
||||
};
|
||||
|
||||
it('returns expected contents when .yml file is present', async () => {
|
||||
const key = path.join(inputDir, 'mkdocs.yml');
|
||||
mockFs({ [key]: mkdocsYml });
|
||||
mockDir.setContent({ 'mkdocs.yml': mkdocsYml });
|
||||
const {
|
||||
path: mkdocsPath,
|
||||
content,
|
||||
configIsTemporary,
|
||||
} = await getMkdocsYml(inputDir, siteOptions);
|
||||
} = await getMkdocsYml(mockDir.path, siteOptions);
|
||||
|
||||
expect(mkdocsPath).toBe(key);
|
||||
expect(mkdocsPath).toBe(mockDir.resolve('mkdocs.yml'));
|
||||
expect(content).toBe(mkdocsYml.toString());
|
||||
expect(configIsTemporary).toBe(false);
|
||||
});
|
||||
|
||||
it('returns expected contents when .yaml file is present', async () => {
|
||||
const key = path.join(inputDir, 'mkdocs.yaml');
|
||||
mockFs({ [key]: mkdocsYml });
|
||||
mockDir.setContent({ 'mkdocs.yaml': mkdocsYml });
|
||||
const {
|
||||
path: mkdocsPath,
|
||||
content,
|
||||
configIsTemporary,
|
||||
} = await getMkdocsYml(inputDir, siteOptions);
|
||||
expect(mkdocsPath).toBe(key);
|
||||
} = await getMkdocsYml(mockDir.path, siteOptions);
|
||||
expect(mkdocsPath).toBe(mockDir.resolve('mkdocs.yaml'));
|
||||
expect(content).toBe(mkdocsYml.toString());
|
||||
expect(configIsTemporary).toBe(false);
|
||||
});
|
||||
@@ -571,17 +573,16 @@ describe('helpers', () => {
|
||||
const defaultSiteOptions = {
|
||||
name: 'Default Test site name',
|
||||
};
|
||||
const key = path.join(inputDir, 'mkdocs.yml');
|
||||
const mockPathExists = jest.spyOn(fs, 'pathExists');
|
||||
mockPathExists.mockImplementation(() => Promise.resolve(false));
|
||||
mockFs({ [key]: mkdocsDefaultYml });
|
||||
mockDir.setContent({ 'mkdocs.yml': mkdocsDefaultYml });
|
||||
const {
|
||||
path: mkdocsPath,
|
||||
content,
|
||||
configIsTemporary,
|
||||
} = await getMkdocsYml(inputDir, defaultSiteOptions);
|
||||
} = await getMkdocsYml(mockDir.path, defaultSiteOptions);
|
||||
|
||||
expect(mkdocsPath).toBe(key);
|
||||
expect(mkdocsPath).toBe(mockDir.resolve('mkdocs.yml'));
|
||||
expect(content.split(/[\r\n]+/g)).toEqual(
|
||||
mkdocsDefaultYml.toString().split(/[\r\n]+/g),
|
||||
);
|
||||
|
||||
@@ -60,6 +60,11 @@ export class DirectoryPreparer implements PreparerBase {
|
||||
this.scmIntegrations = ScmIntegrations.fromConfig(config);
|
||||
}
|
||||
|
||||
/** {@inheritDoc PreparerBase.shouldCleanPreparedDirectory} */
|
||||
shouldCleanPreparedDirectory() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** {@inheritDoc PreparerBase.prepare} */
|
||||
async prepare(
|
||||
entity: Entity,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user