Merge remote-tracking branch 'upstream/master' into airbrake-backend

This commit is contained in:
Karan Shah
2022-02-08 11:27:35 +00:00
78 changed files with 1887 additions and 910 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': minor
---
The `callbackUrl` option of `OAuthAdapter` is now required.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-newrelic-dashboard': patch
---
Export DashboardSnapshotComponent from new-relic-dashboard-plugin
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Removed unnecessary `get-port` dependency
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/core-components': patch
---
Adjust ErrorPage to accept optional supportUrl property to override app support config. Update type of additionalInfo property to be ReactNode to accept both string and component.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': patch
---
Adds a loading bar to the scaffolder task page if the task is still loading. This can happen if it takes a while for a task worker to pick up a task.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
The experimental types build enabled by `--experimental-type-build` now runs in a separate worker thread.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
The file path printed by the default lint formatter is now relative to the repository root, rather than the individual package.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-auth-backend': patch
---
Added a new `cookieConfigurer` option to `AuthProviderConfig` that makes it possible to override the default logic for configuring OAuth provider cookies.
+14
View File
@@ -0,0 +1,14 @@
---
'@backstage/create-app': patch
---
Switched the `file:` dependency for a `link:` dependency in the `backend` package. This makes sure that the `app` package is linked in rather than copied.
To apply this update to an existing app, make the following change to `packages/backend/package.json`:
```diff
"dependencies": {
- "app": "file:../app",
+ "app": "link:../app",
"@backstage/backend-common": "^{{version '@backstage/backend-common'}}",
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-backend-module-msgraph': patch
---
Add userExpand option to allow users to expand fields retrieved from the Graph API - for use in custom transformers
+9
View File
@@ -0,0 +1,9 @@
---
'@backstage/cli': patch
---
Removed the `import/no-duplicates` lint rule from the frontend and backend ESLint configurations. This rule is quite expensive to execute and only provides a purely cosmetic benefit, so we opted to remove it from the set of default rules. If you would like to keep this rule you can add it back in your local ESLint configuration:
```js
'import/no-duplicates': 'warn'
```
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Rather than calling `yarn pack`, the `build-workspace` and `backend-bundle` commands now move files directly whenever possible. This cuts out several `yarn` invocations and speeds the packing process up by several orders of magnitude.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Switched the `lint` command to invoke ESLint directly through its Node.js API rather than spawning a new process.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/cli': patch
---
Introduced an experimental and hidden `repo` sub-command, that contains commands that operate on an entire monorepo rather than individual packages.
+5
View File
@@ -0,0 +1,5 @@
---
'@techdocs/cli': patch
---
Updated the HTTP server to allow for simplification of the development of the CLI itself.
+1 -1
View File
@@ -205,7 +205,7 @@ jobs:
# Grabs the version in the root package.json and creates a tag on GitHub
- name: Create a release tag
id: create_tag
run: node scripts/create-release-tag.js
run: node scripts/create-release-tag.js --dispatch-workflows
env:
GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
@@ -48,3 +48,31 @@ jobs:
git add .
git commit -am "${{ github.event.inputs.version }}"
git push
- name: Dispatch update-helper update
uses: actions/github-script@v5
with:
github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }}
# TODO(Rugvip): Remove the create-app dispatch once we've been on the release version for a while
script: |
console.log('Dispatching upgrade helper sync - release version');
await octokit.actions.createWorkflowDispatch({
owner: 'backstage',
repo: 'upgrade-helper-diff',
workflow_id: 'release.yml',
ref: 'master',
inputs: {
version: require('./package.json').version,
},
});
console.log('Dispatching upgrade helper sync - create-app version');
await octokit.actions.createWorkflowDispatch({
owner: 'backstage',
repo: 'upgrade-helper-diff',
workflow_id: 'release.yml',
ref: 'master',
inputs: {
version: require('./packages/create-app/package.json').version,
},
});
@@ -114,7 +114,7 @@ updated documentation next time you run Backstage!
## Writing and previewing your documentation
Using the [techdocs-cli](https://github.com/backstage/techdocs-cli) you can
Using the [techdocs-cli](https://github.com/backstage/backstage/tree/master/packages/techdocs-cli) you can
preview your docs inside a local Backstage instance and get live reload on
changes. This is useful when you want to preview your documentation while
writing.
-1
View File
@@ -101,7 +101,6 @@
"@types/unzipper": "^0.10.3",
"@types/webpack-env": "^1.15.2",
"aws-sdk-mock": "^5.2.1",
"get-port": "^5.1.1",
"http-errors": "^2.0.0",
"jest": "^26.0.1",
"mock-fs": "^5.1.0",
+1 -1
View File
@@ -61,7 +61,7 @@
"@octokit/rest": "^18.5.3",
"azure-devops-node-api": "^11.0.1",
"dockerode": "^3.3.1",
"example-app": "file:../app",
"example-app": "link:../app",
"express": "^4.17.1",
"express-prom-bundle": "^6.3.6",
"express-promise-router": "^4.1.0",
+1 -12
View File
@@ -54,11 +54,10 @@ module.exports = {
'no-console': 0, // Permitted in console programs
'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()'
'import/newline-after-import': 'error',
'import/no-duplicates': 'warn',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: false,
devDependencies: ['**/*.test.*', 'src/setupTests.*', 'dev/**'],
optionalDependencies: true,
peerDependencies: true,
bundledDependencies: true,
@@ -97,16 +96,6 @@ module.exports = {
{
files: ['*.test.*', 'src/setupTests.*', 'dev/**'],
rules: {
// Tests are allowed to import dev dependencies
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
optionalDependencies: true,
peerDependencies: true,
bundledDependencies: true,
},
],
'no-restricted-syntax': ['error', ...globalRestrictedSyntax],
},
},
+6 -17
View File
@@ -49,11 +49,15 @@ module.exports = {
'@typescript-eslint/no-redeclare': 'error',
'no-undef': 'off',
'import/newline-after-import': 'error',
'import/no-duplicates': 'warn',
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: false,
devDependencies: [
'**/*.test.*',
'**/*.stories.*',
'src/setupTests.*',
'dev/**',
],
optionalDependencies: true,
peerDependencies: true,
bundledDependencies: true,
@@ -101,20 +105,5 @@ module.exports = {
'no-undef': 'off',
},
},
{
files: ['*.test.*', '*.stories.*', 'src/setupTests.*', 'dev/**'],
rules: {
// Tests are allowed to import dev dependencies
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true,
optionalDependencies: true,
peerDependencies: true,
bundledDependencies: true,
},
],
},
},
],
};
+2
View File
@@ -86,6 +86,7 @@
"lodash": "^4.17.21",
"minimatch": "3.0.4",
"mini-css-extract-plugin": "^2.4.2",
"npm-packlist": "^3.0.0",
"node-libs-browser": "^2.2.1",
"ora": "^5.3.0",
"postcss": "^8.1.0",
@@ -132,6 +133,7 @@
"@types/minimatch": "^3.0.5",
"@types/mock-fs": "^4.13.0",
"@types/node": "^14.14.32",
"@types/npm-packlist": "^1.1.2",
"@types/recursive-readdir": "^2.2.0",
"@types/rollup-plugin-peer-deps-external": "^2.2.0",
"@types/rollup-plugin-postcss": "^2.0.0",
+2 -2
View File
@@ -17,7 +17,7 @@
import fs from 'fs-extra';
import { Command } from 'commander';
import { buildBundle } from '../../lib/bundler';
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
import { getEnvironmentParallelism } from '../../lib/parallel';
import { loadCliConfig } from '../../lib/config';
import { paths } from '../../lib/paths';
@@ -25,7 +25,7 @@ export default async (cmd: Command) => {
const { name } = await fs.readJson(paths.resolveTarget('package.json'));
await buildBundle({
entry: 'src/index',
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
parallelism: getEnvironmentParallelism(),
statsJsonEnabled: cmd.stats,
...(await loadCliConfig({
args: cmd.config,
+2 -2
View File
@@ -21,7 +21,7 @@ import tar, { CreateOptions } from 'tar';
import { Command } from 'commander';
import { createDistWorkspace } from '../../lib/packager';
import { paths } from '../../lib/paths';
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
import { getEnvironmentParallelism } from '../../lib/parallel';
import { buildPackage, Output } from '../../lib/builder';
const BUNDLE_FILE = 'bundle.tar.gz';
@@ -40,7 +40,7 @@ export default async (cmd: Command) => {
targetDir: tmpDir,
buildDependencies: Boolean(cmd.buildDependencies),
buildExcludes: [pkg.name],
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
parallelism: getEnvironmentParallelism(),
skeleton: SKELETON_FILE,
});
+9 -6
View File
@@ -15,24 +15,27 @@
*/
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { buildBundle } from '../../lib/bundler';
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
import { getEnvironmentParallelism } from '../../lib/parallel';
import { loadCliConfig } from '../../lib/config';
import { paths } from '../../lib/paths';
interface BuildAppOptions {
targetDir: string;
writeStats: boolean;
configPaths: string[];
}
export async function buildApp(options: BuildAppOptions) {
const { name } = await fs.readJson(paths.resolveTarget('package.json'));
const { targetDir, writeStats, configPaths } = options;
const { name } = await fs.readJson(resolvePath(targetDir, 'package.json'));
await buildBundle({
targetDir,
entry: 'src/index',
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
statsJsonEnabled: options.writeStats,
parallelism: getEnvironmentParallelism(),
statsJsonEnabled: writeStats,
...(await loadCliConfig({
args: options.configPaths,
args: configPaths,
fromPackage: name,
})),
});
@@ -19,20 +19,20 @@ import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import tar, { CreateOptions } from 'tar';
import { createDistWorkspace } from '../../lib/packager';
import { paths } from '../../lib/paths';
import { parseParallel, PARALLEL_ENV_VAR } from '../../lib/parallel';
import { getEnvironmentParallelism } from '../../lib/parallel';
import { buildPackage, Output } from '../../lib/builder';
const BUNDLE_FILE = 'bundle.tar.gz';
const SKELETON_FILE = 'skeleton.tar.gz';
interface BuildBackendOptions {
targetDir: string;
skipBuildDependencies: boolean;
}
export async function buildBackend(options: BuildBackendOptions) {
const targetDir = paths.resolveTarget('dist');
const pkg = await fs.readJson(paths.resolveTarget('package.json'));
const { targetDir, skipBuildDependencies } = options;
const pkg = await fs.readJson(resolvePath(targetDir, 'package.json'));
// We build the target package without generating type declarations.
await buildPackage({ outputs: new Set([Output.cjs]) });
@@ -41,9 +41,9 @@ export async function buildBackend(options: BuildBackendOptions) {
try {
await createDistWorkspace([pkg.name], {
targetDir: tmpDir,
buildDependencies: !options.skipBuildDependencies,
buildDependencies: !skipBuildDependencies,
buildExcludes: [pkg.name],
parallel: parseParallel(process.env[PARALLEL_ENV_VAR]),
parallelism: getEnvironmentParallelism(),
skeleton: SKELETON_FILE,
});
@@ -17,6 +17,7 @@
import { Command } from 'commander';
import { buildPackage, Output } from '../../lib/builder';
import { findRoleFromCommand, getRoleInfo } from '../../lib/role';
import { paths } from '../../lib/paths';
import { buildApp } from './buildApp';
import { buildBackend } from './buildBackend';
@@ -25,12 +26,14 @@ export async function command(cmd: Command): Promise<void> {
if (role === 'app') {
return buildApp({
targetDir: paths.resolveTarget('dist'),
configPaths: cmd.config as string[],
writeStats: Boolean(cmd.stats),
});
}
if (role === 'backend') {
return buildBackend({
targetDir: paths.resolveTarget('dist'),
skipBuildDependencies: Boolean(cmd.skipBuildDependencies),
});
}
+20
View File
@@ -25,6 +25,25 @@ const configOption = [
Array<string>(),
] as const;
export function registerRepoCommand(program: CommanderStatic) {
const command = program
.command('repo [command]', { hidden: true })
.description(
'Command that run across an entire Backstage project [EXPERIMENTAL]',
);
command
.command('build')
.description(
'Build packages in the project, excluding bundled app and backend packages.',
)
.option(
'--all',
'Build all packages, including bundled app and backend packages.',
)
.action(lazy(() => import('./repo/build').then(m => m.command)));
}
export function registerScriptCommand(program: CommanderStatic) {
const command = program
.command('script [command]', { hidden: true })
@@ -312,6 +331,7 @@ export function registerCommands(program: CommanderStatic) {
.description('Print configuration schema')
.action(lazy(() => import('./config/schema').then(m => m.default)));
registerRepoCommand(program);
registerScriptCommand(program);
registerMigrateCommand(program);
+24 -9
View File
@@ -15,19 +15,34 @@
*/
import { Command } from 'commander';
import { run } from '../lib/run';
import { paths } from '../lib/paths';
import { ESLint } from 'eslint';
export default async (cmd: Command, cmdArgs: string[]) => {
const args = [
'--ext=js,jsx,ts,tsx,mjs,cjs',
'--max-warnings=0',
`--format=${cmd.format}`,
...(cmdArgs ?? [paths.targetDir]),
];
const eslint = new ESLint({
cwd: paths.targetDir,
fix: cmd.fix,
extensions: ['js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs'],
});
const results = await eslint.lintFiles(cmdArgs ?? ['.']);
if (cmd.fix) {
args.push('--fix');
await ESLint.outputFixes(results);
}
await run('eslint', args);
const formatter = await eslint.loadFormatter(cmd.format);
// This formatter uses the cwd to format file paths, so let's have that happen from the root instead
if (cmd.format === 'eslint-formatter-friendly') {
process.chdir(paths.targetRoot);
}
const resultText = formatter.format(results);
// If there is any feedback at all, we treat it as a lint failure. This should be
// consistent with our old behavior of passing `--max-warnings=0` when invoking eslint.
if (resultText) {
console.log(resultText);
process.exit(1);
}
};
+167
View File
@@ -0,0 +1,167 @@
/*
* Copyright 2020 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 chalk from 'chalk';
import { Command } from 'commander';
import { relative as relativePath } from 'path';
import { buildPackages, getOutputsForRole } from '../../lib/builder';
import { PackageGraph } from '../../lib/monorepo';
import { ExtendedPackage } from '../../lib/monorepo/PackageGraph';
import { runParallelWorkers } from '../../lib/parallel';
import { paths } from '../../lib/paths';
import { detectRoleFromPackage } from '../../lib/role';
import { buildApp } from '../build/buildApp';
import { buildBackend } from '../build/buildBackend';
function createScriptOptionsParser(anyCmd: Command, commandPath: string[]) {
// Regardless of what command instance is passed in we want to find
// the root command and resolve the path from there
let rootCmd = anyCmd;
while (rootCmd.parent) {
rootCmd = rootCmd.parent;
}
// Now find the command that was requested
let targetCmd = rootCmd as Command | undefined;
for (const name of commandPath) {
targetCmd = targetCmd?.commands.find(c => c.name() === name) as
| Command
| undefined;
}
if (!targetCmd) {
throw new Error(`Could not find script command '${commandPath.join(' ')}'`);
}
const cmd = targetCmd;
const expectedScript = `backstage-cli ${commandPath.join(' ')}`;
return (scriptStr?: string) => {
if (!scriptStr || !scriptStr.startsWith(expectedScript)) {
return undefined;
}
const argsStr = scriptStr.slice(expectedScript.length).trim();
// Can't clone or copy or even use commands as prototype, so we mutate
// the necessary members instead, and then reset them once we're done
const currentOpts = cmd._optionValues;
const currentStore = cmd._storeOptionsAsProperties;
const result: Record<string, any> = {};
cmd._storeOptionsAsProperties = false;
cmd._optionValues = result;
// Triggers the writing of options to the result object
cmd.parseOptions(argsStr.split(' '));
cmd._storeOptionsAsProperties = currentOpts;
cmd._optionValues = currentStore;
return result;
};
}
export async function command(cmd: Command): Promise<void> {
const packages = await PackageGraph.listTargetPackages();
const apps = new Array<ExtendedPackage>();
const backends = new Array<ExtendedPackage>();
const parseBuildScript = createScriptOptionsParser(cmd, ['script', 'build']);
const options = packages.flatMap(pkg => {
const role =
pkg.packageJson.backstage?.role ?? detectRoleFromPackage(pkg.packageJson);
if (!role) {
console.warn(`Ignored ${pkg.packageJson.name} because it has no role`);
return [];
}
if (role === 'app') {
apps.push(pkg);
return [];
} else if (role === 'backend') {
backends.push(pkg);
return [];
}
const outputs = getOutputsForRole(role);
if (outputs.size === 0) {
console.warn(`Ignored ${pkg.packageJson.name} because it has no output`);
return [];
}
const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build);
if (!buildOptions) {
console.warn(
`Ignored ${pkg.packageJson.name} because it does not have a matching build script`,
);
return [];
}
return {
targetDir: pkg.dir,
outputs,
logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `,
minify: buildOptions.minify,
useApiExtractor: buildOptions.experimentalTypeBuild,
};
});
console.log('Building packages');
await buildPackages(options);
if (cmd.all) {
console.log('Building apps');
await runParallelWorkers({
items: apps,
parallelismFactor: 1 / 2,
worker: async pkg => {
const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build);
if (!buildOptions) {
console.warn(
`Ignored ${pkg.packageJson.name} because it does not have a matching build script`,
);
return;
}
await buildApp({
targetDir: pkg.dir,
configPaths: (buildOptions.config as string[]) ?? [],
writeStats: Boolean(buildOptions.stats),
});
},
});
console.log('Building backends');
await runParallelWorkers({
items: backends,
parallelismFactor: 1 / 2,
worker: async pkg => {
const buildOptions = parseBuildScript(pkg.packageJson.scripts?.build);
if (!buildOptions) {
console.warn(
`Ignored ${pkg.packageJson.name} because it does not have a matching build script`,
);
return;
}
await buildBackend({
targetDir: pkg.dir,
skipBuildDependencies: true,
});
},
});
}
}
+11 -11
View File
@@ -127,8 +127,8 @@ describe('bump', () => {
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core-api',
'Some packages are outdated, updating',
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
@@ -252,19 +252,19 @@ describe('bump', () => {
});
expect(logs.filter(Boolean)).toEqual([
'Using custom pattern glob @{backstage,backstage-extra}/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage-extra/custom-two',
'Checking for updates of @backstage-extra/custom',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage-extra/custom',
'Checking for updates of @backstage-extra/custom-two',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core-api',
'Some packages are outdated, updating',
'unlocking @backstage-extra/custom@^1.0.1 ~> 1.1.0',
'unlocking @backstage/core@^1.0.3 ~> 1.0.6',
'unlocking @backstage-extra/custom@^1.0.1 ~> 1.1.0',
'unlocking @backstage/core-api@^1.0.6 ~> 1.0.7',
'unlocking @backstage/core-api@^1.0.3 ~> 1.0.7',
'bumping @backstage-extra/custom-two in a to ^2.0.0',
'bumping @backstage/theme in b to ^2.0.0',
'bumping @backstage-extra/custom-two in b to ^2.0.0',
'bumping @backstage/theme in b to ^2.0.0',
'Running yarn install to install new versions',
'⚠️ The following packages may have breaking changes:',
' @backstage-extra/custom-two : 1.0.0 ~> 2.0.0',
@@ -348,14 +348,14 @@ describe('bump', () => {
});
expect(logs.filter(Boolean)).toEqual([
'Using default pattern glob @backstage/*',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Package info not found, ignoring package @backstage/theme',
'Package info not found, ignoring package @backstage/core',
'Checking for updates of @backstage/theme',
'Checking for updates of @backstage/core',
'Package info not found, ignoring package @backstage/theme',
'Package info not found, ignoring package @backstage/core',
'Package info not found, ignoring package @backstage/theme',
'Checking for updates of @backstage/core',
'Checking for updates of @backstage/theme',
'Package info not found, ignoring package @backstage/core',
'Package info not found, ignoring package @backstage/theme',
'All Backstage packages are up to date!',
]);
+87 -97
View File
@@ -30,6 +30,7 @@ import {
} from '../../lib/versioning';
import { forbiddenDuplicatesFilter } from './lint';
import { BACKSTAGE_JSON } from '@backstage/cli-common';
import { runParallelWorkers } from '../../lib/parallel';
const DEP_TYPES = [
'dependencies',
@@ -68,67 +69,76 @@ export default async (cmd: Command) => {
const versionBumps = new Map<string, PkgVersionInfo[]>();
// Track package versions that we want to remove from yarn.lock in order to trigger a bump
const unlocked = Array<{ name: string; range: string; target: string }>();
await workerThreads(16, dependencyMap.entries(), async ([name, pkgs]) => {
let target: string;
try {
target = await findTargetVersion(name);
} catch (error) {
if (isError(error) && error.name === 'NotFoundError') {
console.log(`Package info not found, ignoring package ${name}`);
return;
}
throw error;
}
for (const pkg of pkgs) {
if (semver.satisfies(target, pkg.range)) {
if (semver.minVersion(pkg.range)?.version !== target) {
unlocked.push({ name, range: pkg.range, target });
await runParallelWorkers({
parallelismFactor: 4,
items: dependencyMap.entries(),
async worker([name, pkgs]) {
let target: string;
try {
target = await findTargetVersion(name);
} catch (error) {
if (isError(error) && error.name === 'NotFoundError') {
console.log(`Package info not found, ignoring package ${name}`);
return;
}
continue;
throw error;
}
versionBumps.set(
pkg.name,
(versionBumps.get(pkg.name) ?? []).concat({
name,
location: pkg.location,
range: `^${target}`, // TODO(Rugvip): Option to use something else than ^?
target,
}),
);
}
for (const pkg of pkgs) {
if (semver.satisfies(target, pkg.range)) {
if (semver.minVersion(pkg.range)?.version !== target) {
unlocked.push({ name, range: pkg.range, target });
}
continue;
}
versionBumps.set(
pkg.name,
(versionBumps.get(pkg.name) ?? []).concat({
name,
location: pkg.location,
range: `^${target}`, // TODO(Rugvip): Option to use something else than ^?
target,
}),
);
}
},
});
const filter = (name: string) => minimatch(name, pattern);
// Check for updates of transitive backstage dependencies
await workerThreads(16, lockfile.keys(), async name => {
// Only check @backstage packages and friends, we don't want this to do a full update of all deps
if (!filter(name)) {
return;
}
let target: string;
try {
target = await findTargetVersion(name);
} catch (error) {
if (isError(error) && error.name === 'NotFoundError') {
console.log(`Package info not found, ignoring package ${name}`);
await runParallelWorkers({
parallelismFactor: 4,
items: lockfile.keys(),
async worker(name) {
// Only check @backstage packages and friends, we don't want this to do a full update of all deps
if (!filter(name)) {
return;
}
throw error;
}
for (const entry of lockfile.get(name) ?? []) {
// Ignore lockfile entries that don't satisfy the version range, since
// these can't cause the package to be locked to an older version
if (!semver.satisfies(target, entry.range)) {
continue;
let target: string;
try {
target = await findTargetVersion(name);
} catch (error) {
if (isError(error) && error.name === 'NotFoundError') {
console.log(`Package info not found, ignoring package ${name}`);
return;
}
throw error;
}
// Unlock all entries that are within range but on the old version
unlocked.push({ name, range: entry.range, target });
}
for (const entry of lockfile.get(name) ?? []) {
// Ignore lockfile entries that don't satisfy the version range, since
// these can't cause the package to be locked to an older version
if (!semver.satisfies(target, entry.range)) {
continue;
}
// Unlock all entries that are within range but on the old version
unlocked.push({ name, range: entry.range, target });
}
},
});
console.log();
@@ -163,38 +173,42 @@ export default async (cmd: Command) => {
}
const breakingUpdates = new Map<string, { from: string; to: string }>();
await workerThreads(16, versionBumps.entries(), async ([name, deps]) => {
const pkgPath = resolvePath(deps[0].location, 'package.json');
const pkgJson = await fs.readJson(pkgPath);
await runParallelWorkers({
parallelismFactor: 4,
items: versionBumps.entries(),
async worker([name, deps]) {
const pkgPath = resolvePath(deps[0].location, 'package.json');
const pkgJson = await fs.readJson(pkgPath);
for (const dep of deps) {
console.log(
`${chalk.cyan('bumping')} ${dep.name} in ${chalk.cyan(
name,
)} to ${chalk.yellow(dep.range)}`,
);
for (const dep of deps) {
console.log(
`${chalk.cyan('bumping')} ${dep.name} in ${chalk.cyan(
name,
)} to ${chalk.yellow(dep.range)}`,
);
for (const depType of DEP_TYPES) {
if (depType in pkgJson && dep.name in pkgJson[depType]) {
const oldRange = pkgJson[depType][dep.name];
pkgJson[depType][dep.name] = dep.range;
for (const depType of DEP_TYPES) {
if (depType in pkgJson && dep.name in pkgJson[depType]) {
const oldRange = pkgJson[depType][dep.name];
pkgJson[depType][dep.name] = dep.range;
// Check if the update was at least a pre-v1 minor or post-v1 major release
const lockfileEntry = lockfile
.get(dep.name)
?.find(entry => entry.range === oldRange);
if (lockfileEntry) {
const from = lockfileEntry.version;
const to = dep.target;
if (!semver.satisfies(to, `^${from}`)) {
breakingUpdates.set(dep.name, { from, to });
// Check if the update was at least a pre-v1 minor or post-v1 major release
const lockfileEntry = lockfile
.get(dep.name)
?.find(entry => entry.range === oldRange);
if (lockfileEntry) {
const from = lockfileEntry.version;
const to = dep.target;
if (!semver.satisfies(to, `^${from}`)) {
breakingUpdates.set(dep.name, { from, to });
}
}
}
}
}
}
await fs.writeJson(pkgPath, pkgJson, { spaces: 2 });
await fs.writeJson(pkgPath, pkgJson, { spaces: 2 });
},
});
console.log();
@@ -324,27 +338,3 @@ export async function bumpBackstageJsonVersion() {
},
);
}
async function workerThreads<T>(
count: number,
items: IterableIterator<T>,
fn: (item: T) => Promise<void>,
) {
const queue = Array.from(items);
async function pop() {
const item = queue.pop();
if (!item) {
return;
}
await fn(item);
await pop();
}
return Promise.all(
Array(count)
.fill(0)
.map(() => pop()),
);
}
@@ -16,146 +16,127 @@
import fs from 'fs-extra';
import chalk from 'chalk';
import {
relative as relativePath,
resolve as resolvePath,
dirname,
} from 'path';
import { Worker } from 'worker_threads';
import { relative as relativePath, resolve as resolvePath } from 'path';
import { paths } from '../paths';
import { buildTypeDefinitionsWorker } from './buildTypeDefinitionsWorker';
// These message types are ignored since we want to avoid duplicating the logic of
// handling them correctly, and we already have the API Reports warning about them.
const ignoredMessages = new Set(['tsdoc-undefined-tag', 'ae-forgotten-export']);
let apiExtractor: undefined | typeof import('@microsoft/api-extractor');
function prepareApiExtractor() {
if (apiExtractor) {
return apiExtractor;
}
try {
apiExtractor = require('@microsoft/api-extractor');
} catch (error) {
throw new Error(
'Failed to resolve @microsoft/api-extractor, it must best installed ' +
'as a dependency of your project in order to use experimental type builds',
);
}
/**
* All of this monkey patching below is because MUI has these bare package.json file as a method
* for making TypeScript accept imports like `@material-ui/core/Button`, and improve tree-shaking
* by declaring them side effect free.
*
* The package.json lookup logic in api-extractor really doesn't like that though, as it enforces
* that the 'name' field exists in all package.json files that it discovers. This below is just
* making sure that we ignore those file package.json files instead of crashing.
*/
const {
PackageJsonLookup,
// eslint-disable-next-line import/no-extraneous-dependencies
} = require('@rushstack/node-core-library/lib/PackageJsonLookup');
const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor;
PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor =
function tryGetPackageJsonFilePathForPatch(path: string) {
if (
path.includes('@material-ui') &&
!dirname(path).endsWith('@material-ui')
) {
return undefined;
}
return old.call(this, path);
};
return apiExtractor!;
}
export async function buildTypeDefinitions() {
const { Extractor, ExtractorConfig } = prepareApiExtractor();
const distTypesPackageDir = paths.resolveTargetRoot(
'dist-types',
relativePath(paths.targetRoot, paths.targetDir),
export async function buildTypeDefinitions(
targetDirs: string[] = [paths.targetDir],
) {
const packageDirs = targetDirs.map(dir =>
relativePath(paths.targetRoot, dir),
);
const entryPoint = resolvePath(distTypesPackageDir, 'src/index.d.ts');
const entryPoints = await Promise.all(
packageDirs.map(async dir => {
const entryPoint = paths.resolveTargetRoot(
'dist-types',
dir,
'src/index.d.ts',
);
const declarationsExist = await fs.pathExists(entryPoint);
if (!declarationsExist) {
const path = relativePath(paths.targetDir, entryPoint);
throw new Error(
`No declaration files found at ${path}, be sure to run ${chalk.bgRed.white(
'yarn tsc',
)} to generate .d.ts files before packaging`,
);
}
const declarationsExist = await fs.pathExists(entryPoint);
if (!declarationsExist) {
throw new Error(
`No declaration files found at ${entryPoint}, be sure to run ${chalk.bgRed.white(
'yarn tsc',
)} to generate .d.ts files before packaging`,
);
}
return entryPoint;
}),
);
const extractorConfig = ExtractorConfig.prepare({
configObject: {
mainEntryPointFilePath: entryPoint,
bundledPackages: [],
const workerConfigs = packageDirs.map(packageDir => {
const targetDir = paths.resolveTargetRoot(packageDir);
const targetTypesDir = paths.resolveTargetRoot('dist-types', packageDir);
const extractorOptions = {
configObject: {
mainEntryPointFilePath: resolvePath(targetTypesDir, 'src/index.d.ts'),
bundledPackages: [],
compiler: {
skipLibCheck: true,
tsconfigFilePath: paths.resolveTargetRoot('tsconfig.json'),
compiler: {
skipLibCheck: true,
tsconfigFilePath: paths.resolveTargetRoot('tsconfig.json'),
},
dtsRollup: {
enabled: true,
untrimmedFilePath: resolvePath(targetDir, 'dist/index.alpha.d.ts'),
betaTrimmedFilePath: resolvePath(targetDir, 'dist/index.beta.d.ts'),
publicTrimmedFilePath: resolvePath(targetDir, 'dist/index.d.ts'),
},
newlineKind: 'lf',
projectFolder: targetDir,
},
dtsRollup: {
enabled: true,
untrimmedFilePath: paths.resolveTarget('dist/index.alpha.d.ts'),
betaTrimmedFilePath: paths.resolveTarget('dist/index.beta.d.ts'),
publicTrimmedFilePath: paths.resolveTarget('dist/index.d.ts'),
},
newlineKind: 'lf',
projectFolder: paths.targetDir,
},
configObjectFullPath: paths.targetDir,
packageJsonFullPath: paths.resolveTarget('package.json'),
configObjectFullPath: targetDir,
packageJsonFullPath: resolvePath(targetDir, 'package.json'),
};
return { extractorOptions, targetTypesDir };
});
const typescriptDir = paths.resolveTargetRoot('node_modules/typescript');
const hasTypescript = await fs.pathExists(typescriptDir);
const extractorResult = Extractor.invoke(extractorConfig, {
typescriptCompilerFolder: hasTypescript ? typescriptDir : undefined,
localBuild: false,
showVerboseMessages: false,
showDiagnostics: false,
messageCallback(message) {
message.handled = true;
if (ignoredMessages.has(message.messageId)) {
return;
}
const typescriptCompilerFolder = hasTypescript ? typescriptDir : undefined;
let text = `${message.text} (${message.messageId})`;
if (message.sourceFilePath) {
text += ' at ';
text += relativePath(distTypesPackageDir, message.sourceFilePath);
if (message.sourceFileLine) {
text += `:${message.sourceFileLine}`;
if (message.sourceFileColumn) {
text += `:${message.sourceFileColumn}`;
}
}
}
if (message.logLevel === 'error') {
console.error(chalk.red(`Error: ${text}`));
} else if (
message.logLevel === 'warning' ||
message.category === 'Extractor'
) {
console.warn(`Warning: ${text}`);
} else {
console.log(text);
}
const worker = new Worker(`(${buildTypeDefinitionsWorker})()`, {
eval: true,
workerData: {
entryPoints,
workerConfigs,
typescriptCompilerFolder,
},
});
if (!extractorResult.succeeded) {
throw new Error(
`Type definition build completed with ${extractorResult.errorCount} errors` +
` and ${extractorResult.warningCount} warnings`,
);
}
await new Promise<void>((resolve, reject) => {
worker.once('error', reject);
worker.once('exit', code => {
if (code) {
reject(new Error(`Worker exited with code ${code}`));
}
});
worker.on('message', data => {
if (data.type === 'done') {
if (data.error) {
reject(data.error);
} else {
resolve();
}
} else if (data.type === 'message') {
const { message, targetTypesDir } = data;
if (ignoredMessages.has(message.messageId)) {
return;
}
let text = `${message.text} (${message.messageId})`;
if (message.sourceFilePath) {
text += ' at ';
text += relativePath(targetTypesDir, message.sourceFilePath);
if (message.sourceFileLine) {
text += `:${message.sourceFileLine}`;
if (message.sourceFileColumn) {
text += `:${message.sourceFileColumn}`;
}
}
}
if (message.logLevel === 'error') {
console.error(chalk.red(`Error: ${text}`));
} else if (
message.logLevel === 'warning' ||
message.category === 'Extractor'
) {
console.warn(`Warning: ${text}`);
} else {
console.log(text);
}
}
});
});
}
@@ -0,0 +1,105 @@
/*
* Copyright 2022 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.
*/
/**
* NOTE: This is a worker thread function that is stringified and executed
* withing a `worker_threads.Worker`. Everything in this function must
* be self-contained.
* Using TypeScript is fine as it is transpiled before being stringified.
*/
export function buildTypeDefinitionsWorker() {
try {
require('@microsoft/api-extractor');
} catch (error) {
throw new Error(
'Failed to resolve @microsoft/api-extractor, it must best installed ' +
'as a dependency of your project in order to use experimental type builds',
);
}
const { dirname } = require('path');
const { workerData, parentPort } = require('worker_threads');
const { entryPoints, workerConfigs, typescriptCompilerFolder } = workerData;
const apiExtractor = require('@microsoft/api-extractor');
const { Extractor, ExtractorConfig, CompilerState } = apiExtractor;
/**
* All of this monkey patching below is because MUI has these bare package.json file as a method
* for making TypeScript accept imports like `@material-ui/core/Button`, and improve tree-shaking
* by declaring them side effect free.
*
* The package.json lookup logic in api-extractor really doesn't like that though, as it enforces
* that the 'name' field exists in all package.json files that it discovers. This below is just
* making sure that we ignore those file package.json files instead of crashing.
*/
const {
PackageJsonLookup,
// eslint-disable-next-line import/no-extraneous-dependencies
} = require('@rushstack/node-core-library/lib/PackageJsonLookup');
const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor;
PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor =
function tryGetPackageJsonFilePathForPatch(path: string) {
if (
path.includes('@material-ui') &&
!dirname(path).endsWith('@material-ui')
) {
return undefined;
}
return old.call(this, path);
};
let success = true;
let compilerState;
for (const { extractorOptions, targetTypesDir } of workerConfigs) {
const extractorConfig = ExtractorConfig.prepare(extractorOptions);
if (!compilerState) {
compilerState = CompilerState.create(extractorConfig, {
additionalEntryPoints: entryPoints,
});
}
const extractorResult = Extractor.invoke(extractorConfig, {
compilerState,
localBuild: false,
typescriptCompilerFolder,
showVerboseMessages: false,
showDiagnostics: false,
messageCallback: (message: any) => {
message.handled = true;
parentPort.postMessage({ type: 'message', message, targetTypesDir });
},
});
if (!extractorResult.succeeded) {
parentPort.postMessage({
type: 'done',
error: new Error(
`Type definition build completed with ${extractorResult.errorCount} errors` +
` and ${extractorResult.warningCount} warnings`,
),
});
success = false;
break;
}
}
if (success) {
parentPort.postMessage({ type: 'done' });
}
}
+24 -8
View File
@@ -16,7 +16,7 @@
import chalk from 'chalk';
import fs from 'fs-extra';
import { relative as relativePath } from 'path';
import { relative as relativePath, resolve as resolvePath } from 'path';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
@@ -26,7 +26,7 @@ import svgr from '@svgr/rollup';
import dts from 'rollup-plugin-dts';
import json from '@rollup/plugin-json';
import yaml from '@rollup/plugin-yaml';
import { RollupOptions, OutputOptions } from 'rollup';
import { RollupOptions, OutputOptions, RollupWarning } from 'rollup';
import { forwardFileImports } from './plugins';
import { BuildOptions, Output } from './types';
@@ -37,6 +37,19 @@ export async function makeRollupConfigs(
options: BuildOptions,
): Promise<RollupOptions[]> {
const configs = new Array<RollupOptions>();
const targetDir = options.targetDir ?? paths.targetDir;
const onwarn = ({ code, message }: RollupWarning) => {
if (code === 'EMPTY_BUNDLE') {
return; // We don't care about this one
}
if (options.logPrefix) {
console.log(options.logPrefix + message);
} else {
console.log(message);
}
};
const distDir = resolvePath(targetDir, 'dist');
if (options.outputs.has(Output.cjs) || options.outputs.has(Output.esm)) {
const output = new Array<OutputOptions>();
@@ -44,7 +57,7 @@ export async function makeRollupConfigs(
if (options.outputs.has(Output.cjs)) {
output.push({
dir: 'dist',
dir: distDir,
entryFileNames: 'index.cjs.js',
chunkFileNames: 'cjs/[name]-[hash].cjs.js',
format: 'commonjs',
@@ -53,7 +66,7 @@ export async function makeRollupConfigs(
}
if (options.outputs.has(Output.esm)) {
output.push({
dir: 'dist',
dir: distDir,
entryFileNames: 'index.esm.js',
chunkFileNames: 'esm/[name]-[hash].esm.js',
format: 'module',
@@ -64,12 +77,14 @@ export async function makeRollupConfigs(
}
configs.push({
input: 'src/index.ts',
input: resolvePath(targetDir, 'src/index.ts'),
output,
onwarn,
preserveEntrySignatures: 'strict',
external: require('module').builtinModules,
plugins: [
peerDepsExternal({
packageJsonPath: resolvePath(targetDir, 'package.json'),
includeDependencies: true,
}),
resolve({ mainFields }),
@@ -109,13 +124,13 @@ export async function makeRollupConfigs(
if (options.outputs.has(Output.types) && !options.useApiExtractor) {
const typesInput = paths.resolveTargetRoot(
'dist-types',
relativePath(paths.targetRoot, paths.targetDir),
relativePath(paths.targetRoot, targetDir),
'src/index.d.ts',
);
const declarationsExist = await fs.pathExists(typesInput);
if (!declarationsExist) {
const path = relativePath(paths.targetDir, typesInput);
const path = relativePath(targetDir, typesInput);
throw new Error(
`No declaration files found at ${path}, be sure to run ${chalk.bgRed.white(
'yarn tsc',
@@ -126,9 +141,10 @@ export async function makeRollupConfigs(
configs.push({
input: typesInput,
output: {
file: 'dist/index.d.ts',
file: resolvePath(distDir, 'index.d.ts'),
format: 'es',
},
onwarn,
plugins: [dts()],
});
}
+1 -1
View File
@@ -14,6 +14,6 @@
* limitations under the License.
*/
export { buildPackage } from './packager';
export { buildPackage, buildPackages, getOutputsForRole } from './packager';
export { Output } from './types';
export type { BuildOptions } from './types';
+51 -1
View File
@@ -17,11 +17,13 @@
import fs from 'fs-extra';
import { rollup, RollupOptions } from 'rollup';
import chalk from 'chalk';
import { relative as relativePath } from 'path';
import { relative as relativePath, resolve as resolvePath } from 'path';
import { paths } from '../paths';
import { makeRollupConfigs } from './config';
import { BuildOptions, Output } from './types';
import { buildTypeDefinitions } from './buildTypeDefinitions';
import { getRoleInfo } from '../role';
import { runParallelWorkers } from '../parallel';
export function formatErrorMessage(error: any) {
let msg = '';
@@ -116,3 +118,51 @@ export const buildPackage = async (options: BuildOptions) => {
await Promise.all(buildTasks);
};
export const buildPackages = async (options: BuildOptions[]) => {
if (options.some(opt => !opt.targetDir)) {
throw new Error('targetDir must be set for all build options');
}
const rollupConfigs = await Promise.all(options.map(makeRollupConfigs));
await Promise.all(
options.map(({ targetDir }) => fs.remove(resolvePath(targetDir!, 'dist'))),
);
const buildTasks = rollupConfigs.flat().map(opts => () => rollupBuild(opts));
const typeDefinitionTargetDirs = options
.filter(
({ outputs, useApiExtractor }) =>
outputs.has(Output.types) && useApiExtractor,
)
.map(_ => _.targetDir!);
if (typeDefinitionTargetDirs.length > 0) {
// Make sure this one is started first
buildTasks.unshift(() => buildTypeDefinitions(typeDefinitionTargetDirs));
}
await runParallelWorkers({
items: buildTasks,
worker: async task => task(),
});
};
export function getOutputsForRole(role: string): Set<Output> {
const outputs = new Set<Output>();
for (const output of getRoleInfo(role).output) {
if (output === 'cjs') {
outputs.add(Output.cjs);
}
if (output === 'esm') {
outputs.add(Output.esm);
}
if (output === 'types') {
outputs.add(Output.types);
}
}
return outputs;
}
+2
View File
@@ -21,6 +21,8 @@ export enum Output {
}
export type BuildOptions = {
logPrefix?: string;
targetDir?: string;
outputs: Set<Output>;
minify?: boolean;
useApiExtractor?: boolean;
+1 -1
View File
@@ -93,7 +93,7 @@ export async function createConfig(
if (checksEnabled) {
plugins.push(
new ForkTsCheckerWebpackPlugin({
typescript: { configFile: paths.targetTsConfig },
typescript: { configFile: paths.targetTsConfig, memoryLimit: 4096 },
}),
new ESLintPlugin({
context: paths.targetPath,
+16 -13
View File
@@ -15,55 +15,58 @@
*/
import fs from 'fs-extra';
import { resolve as resolvePath } from 'path';
import { paths } from '../paths';
export type BundlingPathsOptions = {
// bundle entrypoint, e.g. 'src/index'
entry: string;
// Target directory, defaulting to paths.targetDir
targetDir?: string;
};
export function resolveBundlingPaths(options: BundlingPathsOptions) {
const { entry } = options;
const { entry, targetDir = paths.targetDir } = options;
const resolveTargetModule = (pathString: string) => {
for (const ext of ['mjs', 'js', 'ts', 'tsx', 'jsx']) {
const filePath = paths.resolveTarget(`${pathString}.${ext}`);
const filePath = resolvePath(targetDir, `${pathString}.${ext}`);
if (fs.pathExistsSync(filePath)) {
return filePath;
}
}
return paths.resolveTarget(`${pathString}.js`);
return resolvePath(targetDir, `${pathString}.js`);
};
let targetPublic = undefined;
let targetHtml = paths.resolveTarget('public/index.html');
let targetHtml = resolvePath(targetDir, 'public/index.html');
// Prefer public folder
if (fs.pathExistsSync(targetHtml)) {
targetPublic = paths.resolveTarget('public');
targetPublic = resolvePath(targetDir, 'public');
} else {
targetHtml = paths.resolveTarget(`${entry}.html`);
targetHtml = resolvePath(targetDir, `${entry}.html`);
if (!fs.pathExistsSync(targetHtml)) {
targetHtml = paths.resolveOwn('templates/serve_index.html');
}
}
// Backend plugin dev run file
const targetRunFile = paths.resolveTarget('src/run.ts');
const targetRunFile = resolvePath(targetDir, 'src/run.ts');
const runFileExists = fs.pathExistsSync(targetRunFile);
return {
targetHtml,
targetPublic,
targetPath: paths.resolveTarget('.'),
targetPath: resolvePath(targetDir, '.'),
targetRunFile: runFileExists ? targetRunFile : undefined,
targetDist: paths.resolveTarget('dist'),
targetAssets: paths.resolveTarget('assets'),
targetSrc: paths.resolveTarget('src'),
targetDev: paths.resolveTarget('dev'),
targetDist: resolvePath(targetDir, 'dist'),
targetAssets: resolvePath(targetDir, 'assets'),
targetSrc: resolvePath(targetDir, 'src'),
targetDev: resolvePath(targetDir, 'dev'),
targetEntry: resolveTargetModule(entry),
targetTsConfig: paths.resolveTargetRoot('tsconfig.json'),
targetPackageJson: paths.resolveTarget('package.json'),
targetPackageJson: resolvePath(targetDir, 'package.json'),
rootNodeModules: paths.resolveTargetRoot('node_modules'),
root: paths.targetRoot,
};
+5 -4
View File
@@ -16,7 +16,6 @@
import { AppConfig, Config } from '@backstage/config';
import { BundlingPathsOptions } from './paths';
import { ParallelOption } from '../parallel';
import { ConfigSchema } from '@backstage/config-loader';
export type BundlingOptions = {
@@ -25,7 +24,7 @@ export type BundlingOptions = {
frontendConfig: Config;
frontendAppConfigs: AppConfig[];
baseUrl: URL;
parallel?: ParallelOption;
parallelism?: number;
};
export type ServeOptions = BundlingPathsOptions & {
@@ -35,8 +34,10 @@ export type ServeOptions = BundlingPathsOptions & {
};
export type BuildOptions = BundlingPathsOptions & {
// Target directory, defaulting to paths.targetDir
targetDir?: string;
statsJsonEnabled: boolean;
parallel?: ParallelOption;
parallelism?: number;
schema?: ConfigSchema;
frontendConfig: Config;
frontendAppConfigs: AppConfig[];
@@ -45,7 +46,7 @@ export type BuildOptions = BundlingPathsOptions & {
export type BackendBundlingOptions = {
checksEnabled: boolean;
isDev: boolean;
parallel?: ParallelOption;
parallelism?: number;
inspectEnabled: boolean;
inspectBrkEnabled: boolean;
};
@@ -0,0 +1,88 @@
/*
* Copyright 2022 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 fs from 'fs-extra';
import npmPackList from 'npm-packlist';
import { join as joinPath, resolve as resolvePath } from 'path';
const SKIPPED_KEYS = ['access', 'registry', 'tag', 'alphaTypes', 'betaTypes'];
// Writes e.g. alpha/package.json
async function writeReleaseStageEntrypoint(
pkg: any,
stage: 'alpha' | 'beta',
targetDir: string,
) {
await fs.ensureDir(resolvePath(targetDir, stage));
await fs.writeJson(
resolvePath(targetDir, stage, 'package.json'),
{
name: pkg.name,
version: pkg.version,
main: (pkg.publishConfig.main || pkg.main) && '..',
module: (pkg.publishConfig.module || pkg.module) && '..',
browser: (pkg.publishConfig.browser || pkg.browser) && '..',
types: joinPath('..', pkg.publishConfig[`${stage}Types`]),
},
{ encoding: 'utf8', spaces: 2 },
);
}
export async function copyPackageDist(packageDir: string, targetDir: string) {
const pkgPath = resolvePath(packageDir, 'package.json');
const pkgContent = await fs.readFile(pkgPath, 'utf8');
const pkg = JSON.parse(pkgContent);
const publishConfig = pkg.publishConfig ?? {};
for (const key of Object.keys(publishConfig)) {
if (!SKIPPED_KEYS.includes(key)) {
pkg[key] = publishConfig[key];
}
}
// We remove the dependencies from package.json of packages that are marked
// as bundled, so that yarn doesn't try to install them.
if (pkg.bundled) {
delete pkg.dependencies;
delete pkg.devDependencies;
delete pkg.peerDependencies;
delete pkg.optionalDependencies;
}
// Write the modified package.json so that the file listing is correct
await fs.writeJson(pkgPath, pkg, { encoding: 'utf8', spaces: 2 });
// Lists all dist files, respecting .npmignore, files field in package.json, etc.
const filePaths = await npmPackList({ path: packageDir });
await fs.ensureDir(targetDir);
for (const filePath of filePaths.sort()) {
await fs.copy(
resolvePath(packageDir, filePath),
resolvePath(targetDir, filePath),
);
}
if (publishConfig.alphaTypes) {
await writeReleaseStageEntrypoint(pkg, 'alpha', targetDir);
}
if (publishConfig.betaTypes) {
await writeReleaseStageEntrypoint(pkg, 'beta', targetDir);
}
// Restore package.json
await fs.writeFile(pkgPath, pkgContent, 'utf8');
}
@@ -0,0 +1,303 @@
/*
* Copyright 2020 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 chalk from 'chalk';
import fs from 'fs-extra';
import {
join as joinPath,
resolve as resolvePath,
relative as relativePath,
} from 'path';
import { tmpdir } from 'os';
import tar, { CreateOptions } from 'tar';
import partition from 'lodash/partition';
import { paths } from '../paths';
import { run } from '../run';
import {
dependencies as cliDependencies,
devDependencies as cliDevDependencies,
} from '../../../package.json';
import { PackageGraph, PackageGraphNode } from '../monorepo';
import {
BuildOptions,
buildPackages,
getOutputsForRole,
Output,
} from '../builder';
import { copyPackageDist } from './copyPackageDist';
// These packages aren't safe to pack in parallel since the CLI depends on them
const UNSAFE_PACKAGES = [
...Object.keys(cliDependencies),
...Object.keys(cliDevDependencies),
];
type FileEntry =
| string
| {
src: string;
dest: string;
};
type Options = {
/**
* Target directory for the dist workspace, defaults to a temporary directory
*/
targetDir?: string;
/**
* Files to copy into the target workspace.
*
* Defaults to ['yarn.lock', 'package.json'].
*/
files?: FileEntry[];
/**
* If set to true, the target packages are built before they are packaged into the workspace.
*/
buildDependencies?: boolean;
/**
* When `buildDependencies` is set, this list of packages will not be built even if they are dependencies.
*/
buildExcludes?: string[];
/**
* Controls amount of parallelism in some build steps.
*/
parallelism?: number;
/**
* If set, creates a skeleton tarball that contains all package.json files
* with the same structure as the workspace dir.
*/
skeleton?: 'skeleton.tar' | 'skeleton.tar.gz';
};
/**
* Uses `yarn pack` to package local packages and unpacks them into a dist workspace.
* The target workspace will end up containing dist version of each package and
* will be suitable for packaging e.g. into a docker image.
*
* This creates a structure that is functionally similar to if the packages were
* installed from npm, but uses Yarn workspaces to link to them at runtime.
*/
export async function createDistWorkspace(
packageNames: string[],
options: Options = {},
) {
const targetDir =
options.targetDir ??
(await fs.mkdtemp(resolvePath(tmpdir(), 'dist-workspace')));
const packages = await PackageGraph.listTargetPackages();
const packageGraph = PackageGraph.fromPackages(packages);
const targetNames = packageGraph.collectPackageNames(packageNames, node => {
// Don't include dependencies of packages that are marked as bundled
if (node.packageJson.bundled) {
return undefined;
}
return node.publishedLocalDependencies.keys();
});
const targets = Array.from(targetNames).map(name => packageGraph.get(name)!);
if (options.buildDependencies) {
const exclude = options.buildExcludes ?? [];
const toBuild = new Set(
targets.map(_ => _.name).filter(name => !exclude.includes(name)),
);
const standardBuilds = new Array<BuildOptions>();
const customBuild = new Array<string>();
for (const pkg of packages) {
if (!toBuild.has(pkg.packageJson.name)) {
continue;
}
const role = pkg.packageJson.backstage?.role;
if (!role) {
console.warn(`Ignored ${pkg.packageJson.name} because it has no role`);
customBuild.push(pkg.packageJson.name);
continue;
}
const buildScript = pkg.packageJson.scripts?.build;
if (!buildScript) {
customBuild.push(pkg.packageJson.name);
continue;
}
if (!buildScript.startsWith('backstage-cli script build')) {
console.warn(
`Ignored ${pkg.packageJson.name} because it has a custom build script, '${buildScript}'`,
);
customBuild.push(pkg.packageJson.name);
continue;
}
const outputs = getOutputsForRole(role);
// No need to build and include types in the production runtime
outputs.delete(Output.types);
if (outputs.size > 0) {
standardBuilds.push({
targetDir: pkg.dir,
outputs: outputs,
logPrefix: `${chalk.cyan(relativePath(paths.targetRoot, pkg.dir))}: `,
// No need to detect these for the backend builds, we assume no minification or types
minify: false,
useApiExtractor: false,
});
}
}
await buildPackages(standardBuilds);
if (customBuild.length > 0) {
const scopeArgs = customBuild.flatMap(name => ['--scope', name]);
const lernaArgs =
options.parallelism && Number.isInteger(options.parallelism)
? ['--concurrency', options.parallelism.toString()]
: [];
await run('yarn', ['lerna', ...lernaArgs, 'run', ...scopeArgs, 'build'], {
cwd: paths.targetRoot,
});
}
}
await moveToDistWorkspace(targetDir, targets);
const files: FileEntry[] = options.files ?? ['yarn.lock', 'package.json'];
for (const file of files) {
const src = typeof file === 'string' ? file : file.src;
const dest = typeof file === 'string' ? file : file.dest;
await fs.copy(paths.resolveTargetRoot(src), resolvePath(targetDir, dest));
}
if (options.skeleton) {
const skeletonFiles = targets.map(target => {
const dir = relativePath(paths.targetRoot, target.dir);
return joinPath(dir, 'package.json');
});
await tar.create(
{
file: resolvePath(targetDir, options.skeleton),
cwd: targetDir,
portable: true,
noMtime: true,
gzip: options.skeleton.endsWith('.gz'),
} as CreateOptions & { noMtime: boolean },
skeletonFiles,
);
}
return targetDir;
}
const FAST_PACK_SCRIPTS = [
undefined,
'backstage-cli prepack',
'backstage-cli script prepack',
];
async function moveToDistWorkspace(
workspaceDir: string,
localPackages: PackageGraphNode[],
): Promise<void> {
const [fastPackPackages, slowPackPackages] = partition(localPackages, pkg =>
FAST_PACK_SCRIPTS.includes(pkg.packageJson.scripts?.prepack),
);
// New an improved flow where we avoid calling `yarn pack`
await Promise.all(
fastPackPackages.map(async target => {
console.log(`Moving ${target.name} into dist workspace`);
const outputDir = relativePath(paths.targetRoot, target.dir);
const absoluteOutputPath = resolvePath(workspaceDir, outputDir);
await copyPackageDist(target.dir, absoluteOutputPath);
}),
);
// Old flow is below, which calls `yarn pack` and extracts the tarball
async function pack(target: PackageGraphNode, archive: string) {
console.log(`Repacking ${target.name} into dist workspace`);
const archivePath = resolvePath(workspaceDir, archive);
await run('yarn', ['pack', '--filename', archivePath], {
cwd: target.dir,
});
// TODO(Rugvip): yarn pack doesn't call postpack, once the bug is fixed this can be removed
if (target.packageJson?.scripts?.postpack) {
await run('yarn', ['postpack'], { cwd: target.dir });
}
const outputDir = relativePath(paths.targetRoot, target.dir);
const absoluteOutputPath = resolvePath(workspaceDir, outputDir);
await fs.ensureDir(absoluteOutputPath);
await tar.extract({
file: archivePath,
cwd: absoluteOutputPath,
strip: 1,
});
await fs.remove(archivePath);
// We remove the dependencies from package.json of packages that are marked
// as bundled, so that yarn doesn't try to install them.
if (target.packageJson.bundled) {
const pkgJson = await fs.readJson(
resolvePath(absoluteOutputPath, 'package.json'),
);
delete pkgJson.dependencies;
delete pkgJson.devDependencies;
delete pkgJson.peerDependencies;
delete pkgJson.optionalDependencies;
await fs.writeJson(
resolvePath(absoluteOutputPath, 'package.json'),
pkgJson,
{
spaces: 2,
},
);
}
}
const [unsafePackages, safePackages] = partition(slowPackPackages, p =>
UNSAFE_PACKAGES.includes(p.name),
);
// The unsafe package are packed first one by one in order to avoid race conditions
// where the CLI is being executed with broken dependencies.
for (const target of unsafePackages) {
await pack(target, `temp-package.tgz`);
}
// Repacking in parallel is much faster and safe for all packages outside of the Backstage repo
await Promise.all(
safePackages.map(async (target, index) =>
pack(target, `temp-package-${index}.tgz`),
),
);
}
+2 -211
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* Copyright 2022 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.
@@ -14,213 +14,4 @@
* limitations under the License.
*/
import fs from 'fs-extra';
import {
join as joinPath,
resolve as resolvePath,
relative as relativePath,
} from 'path';
import { tmpdir } from 'os';
import tar, { CreateOptions } from 'tar';
import { paths } from '../paths';
import { run } from '../run';
import { ParallelOption } from '../parallel';
import {
dependencies as cliDependencies,
devDependencies as cliDevDependencies,
} from '../../../package.json';
import { getPackages } from '@manypkg/get-packages';
import { PackageGraph, PackageGraphNode } from '../monorepo';
// These packages aren't safe to pack in parallel since the CLI depends on them
const UNSAFE_PACKAGES = [
...Object.keys(cliDependencies),
...Object.keys(cliDevDependencies),
];
type FileEntry =
| string
| {
src: string;
dest: string;
};
type Options = {
/**
* Target directory for the dist workspace, defaults to a temporary directory
*/
targetDir?: string;
/**
* Files to copy into the target workspace.
*
* Defaults to ['yarn.lock', 'package.json'].
*/
files?: FileEntry[];
/**
* If set to true, the target packages are built before they are packaged into the workspace.
*/
buildDependencies?: boolean;
/**
* When `buildDependencies` is set, this list of packages will not be built even if they are dependencies.
*/
buildExcludes?: string[];
/**
* Enable (true/false) or control amount of (number) parallelism in some build steps.
*/
parallel?: ParallelOption;
/**
* If set, creates a skeleton tarball that contains all package.json files
* with the same structure as the workspace dir.
*/
skeleton?: 'skeleton.tar' | 'skeleton.tar.gz';
};
/**
* Uses `yarn pack` to package local packages and unpacks them into a dist workspace.
* The target workspace will end up containing dist version of each package and
* will be suitable for packaging e.g. into a docker image.
*
* This creates a structure that is functionally similar to if the packages where
* installed from npm, but uses Yarn workspaces to link to them at runtime.
*/
export async function createDistWorkspace(
packageNames: string[],
options: Options = {},
) {
const targetDir =
options.targetDir ??
(await fs.mkdtemp(resolvePath(tmpdir(), 'dist-workspace')));
const { packages } = await getPackages(paths.targetDir);
const packageGraph = PackageGraph.fromPackages(packages);
const targetNames = packageGraph.collectPackageNames(packageNames, node => {
// Don't include dependencies of packages that are marked as bundled
if (node.packageJson.bundled) {
return undefined;
}
return node.publishedLocalDependencies.keys();
});
const targets = Array.from(targetNames).map(name => packageGraph.get(name)!);
if (options.buildDependencies) {
const exclude = options.buildExcludes ?? [];
const toBuild = targets.filter(target => !exclude.includes(target.name));
if (toBuild.length > 0) {
const scopeArgs = toBuild.flatMap(target => ['--scope', target.name]);
const lernaArgs =
options.parallel && Number.isInteger(options.parallel)
? ['--concurrency', options.parallel.toString()]
: [];
await run('yarn', ['lerna', ...lernaArgs, 'run', ...scopeArgs, 'build'], {
cwd: paths.targetRoot,
});
}
}
await moveToDistWorkspace(targetDir, targets);
const files: FileEntry[] = options.files ?? ['yarn.lock', 'package.json'];
for (const file of files) {
const src = typeof file === 'string' ? file : file.src;
const dest = typeof file === 'string' ? file : file.dest;
await fs.copy(paths.resolveTargetRoot(src), resolvePath(targetDir, dest));
}
if (options.skeleton) {
const skeletonFiles = targets.map(target => {
const dir = relativePath(paths.targetRoot, target.dir);
return joinPath(dir, 'package.json');
});
await tar.create(
{
file: resolvePath(targetDir, options.skeleton),
cwd: targetDir,
portable: true,
noMtime: true,
gzip: options.skeleton.endsWith('.gz'),
} as CreateOptions & { noMtime: boolean },
skeletonFiles,
);
}
return targetDir;
}
async function moveToDistWorkspace(
workspaceDir: string,
localPackages: PackageGraphNode[],
): Promise<void> {
async function pack(target: PackageGraphNode, archive: string) {
console.log(`Repacking ${target.name} into dist workspace`);
const archivePath = resolvePath(workspaceDir, archive);
await run('yarn', ['pack', '--filename', archivePath], {
cwd: target.dir,
});
// TODO(Rugvip): yarn pack doesn't call postpack, once the bug is fixed this can be removed
if (target.packageJson?.scripts?.postpack) {
await run('yarn', ['postpack'], { cwd: target.dir });
}
const outputDir = relativePath(paths.targetRoot, target.dir);
const absoluteOutputPath = resolvePath(workspaceDir, outputDir);
await fs.ensureDir(absoluteOutputPath);
await tar.extract({
file: archivePath,
cwd: absoluteOutputPath,
strip: 1,
});
await fs.remove(archivePath);
// We remove the dependencies from package.json of packages that are marked
// as bundled, so that yarn doesn't try to install them.
if (target.packageJson.bundled) {
const pkgJson = await fs.readJson(
resolvePath(absoluteOutputPath, 'package.json'),
);
delete pkgJson.dependencies;
delete pkgJson.devDependencies;
delete pkgJson.peerDependencies;
delete pkgJson.optionalDependencies;
await fs.writeJson(
resolvePath(absoluteOutputPath, 'package.json'),
pkgJson,
{
spaces: 2,
},
);
}
}
const unsafePackages = localPackages.filter(p =>
UNSAFE_PACKAGES.includes(p.name),
);
const safePackages = localPackages.filter(
p => !UNSAFE_PACKAGES.includes(p.name),
);
// The unsafe package are packed first one by one in order to avoid race conditions
// where the CLI is being executed with broken dependencies.
for (const target of unsafePackages) {
await pack(target, `temp-package.tgz`);
}
// Repacking in parallel is much faster and safe for all packages outside of the Backstage repo
await Promise.all(
safePackages.map(async (target, index) =>
pack(target, `temp-package-${index}.tgz`),
),
);
}
export { createDistWorkspace } from './createDistWorkspace';
+122 -37
View File
@@ -14,46 +14,131 @@
* limitations under the License.
*/
import { isParallelDefault, parseParallel } from './parallel';
import {
parseParallelismOption,
getEnvironmentParallelism,
runParallelWorkers,
} from './parallel';
describe('parallel', () => {
describe('parseParallel', () => {
it('coerces "false" string to boolean', () => {
expect(parseParallel('false')).toBeFalsy();
});
it('coerces "true" to boolean', () => {
expect(parseParallel('true')).toBeTruthy();
});
it('coerces number string to number', () => {
expect(parseParallel('2')).toBe(2);
});
it.each([[true], [false], [2]])('returns itself for %p', value => {
expect(parseParallel(value as any)).toEqual(value);
});
it.each([[undefined], [null]])('returns true for %p', value => {
expect(parseParallel(value as any)).toBe(true);
});
it.each([['on'], [2.5], ['2.5']])('throws error for %p', value => {
expect(() => parseParallel(value as any)).toThrowError(
`Parallel option value '${value}' is not a boolean or integer`,
);
});
describe('parseParallelismOption', () => {
it('coerces false no parallelism', () => {
expect(parseParallelismOption(false)).toBe(1);
expect(parseParallelismOption('false')).toBe(1);
});
describe('isParallelDefault', () => {
it('returns true if default value', () => {
expect(isParallelDefault(undefined)).toBeTruthy();
expect(isParallelDefault(true)).toBeTruthy();
});
it('coerces true or undefined to default parallelism', () => {
expect(parseParallelismOption(true)).toBe(4);
expect(parseParallelismOption('true')).toBe(4);
expect(parseParallelismOption(undefined)).toBe(4);
expect(parseParallelismOption(null)).toBe(4);
});
it('returns false if not default value', () => {
expect(isParallelDefault(false)).toBeFalsy();
expect(isParallelDefault(2)).toBeFalsy();
expect(isParallelDefault('true' as any)).toBeFalsy();
});
it('coerces number string to number', () => {
expect(parseParallelismOption('2')).toBe(2);
});
it.each([['on'], [2.5], ['2.5']])('throws error for %p', value => {
expect(() => parseParallelismOption(value as any)).toThrowError(
`Parallel option value '${value}' is not a boolean or integer`,
);
});
});
describe('getEnvironmentParallelism', () => {
it('reads the parallelism setting from the environment', () => {
process.env.BACKSTAGE_CLI_BUILD_PARALLEL = '2';
expect(getEnvironmentParallelism()).toBe(2);
process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'true';
expect(getEnvironmentParallelism()).toBe(4);
process.env.BACKSTAGE_CLI_BUILD_PARALLEL = 'false';
expect(getEnvironmentParallelism()).toBe(1);
delete process.env.BACKSTAGE_CLI_BUILD_PARALLEL;
expect(getEnvironmentParallelism()).toBe(4);
});
});
describe('runParallelWorkers', () => {
it('executes work in parallel', async () => {
const started = new Array<number>();
const done = new Array<number>();
const waiting = new Array<() => void>();
const work = runParallelWorkers({
items: [0, 1, 2, 3, 4],
parallelismFactor: 0.5, // 2 at a time
worker: async item => {
started.push(item);
await new Promise<void>(resolve => {
waiting[item] = resolve;
});
done.push(item);
},
});
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1]);
expect(done).toEqual([]);
waiting[0]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2]);
expect(done).toEqual([0]);
waiting[1]();
waiting[2]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3, 4]);
expect(done).toEqual([0, 1, 2]);
waiting[3]();
waiting[4]();
await work;
expect(done).toEqual([0, 1, 2, 3, 4]);
});
it('executes work sequentially', async () => {
const started = new Array<number>();
const done = new Array<number>();
const waiting = new Array<() => void>();
const work = runParallelWorkers({
items: [0, 1, 2, 3, 4],
parallelismFactor: 0, // 1 at a time
worker: async item => {
started.push(item);
await new Promise<void>(resolve => {
waiting[item] = resolve;
});
done.push(item);
},
});
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0]);
expect(done).toEqual([]);
waiting[0]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1]);
expect(done).toEqual([0]);
waiting[1]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2]);
waiting[2]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3]);
waiting[3]();
await new Promise(resolve => setTimeout(resolve));
expect(started).toEqual([0, 1, 2, 3, 4]);
waiting[4]();
await work;
expect(done).toEqual([0, 1, 2, 3, 4]);
});
});
+57 -14
View File
@@ -14,30 +14,31 @@
* limitations under the License.
*/
export const DEFAULT_PARALLELISM = 4;
export const PARALLEL_ENV_VAR = 'BACKSTAGE_CLI_BUILD_PARALLEL';
export type ParallelOption = boolean | number | undefined;
export type ParallelismOption = boolean | string | number | null | undefined;
export function isParallelDefault(parallel: ParallelOption) {
return parallel === undefined || parallel === true;
}
export function parseParallel(
parallel: boolean | string | number | undefined,
): ParallelOption {
export function parseParallelismOption(parallel: ParallelismOption): number {
if (parallel === undefined || parallel === null) {
return true;
return DEFAULT_PARALLELISM;
} else if (typeof parallel === 'boolean') {
return parallel;
return parallel ? DEFAULT_PARALLELISM : 1;
} else if (typeof parallel === 'number' && Number.isInteger(parallel)) {
if (parallel < 1) {
return 1;
}
return parallel;
} else if (typeof parallel === 'string') {
if (parallel === 'true') {
return true;
return parseParallelismOption(true);
} else if (parallel === 'false') {
return false;
} else if (Number.isInteger(parseFloat(parallel.toString()))) {
return Number(parallel);
return parseParallelismOption(false);
}
const parsed = Number(parallel);
if (Number.isInteger(parsed)) {
return parseParallelismOption(parsed);
}
}
@@ -45,3 +46,45 @@ export function parseParallel(
`Parallel option value '${parallel}' is not a boolean or integer`,
);
}
export function getEnvironmentParallelism() {
return parseParallelismOption(process.env[PARALLEL_ENV_VAR]);
}
type ParallelWorkerOptions<TItem> = {
/**
* Decides the number of parallel workers by multiplying
* this with the configured parallelism, which defaults to 4.
*
* Defaults to 1.
*/
parallelismFactor?: number;
parallelismSetting?: ParallelismOption;
items: Iterable<TItem>;
worker: (item: TItem) => Promise<void>;
};
export async function runParallelWorkers<TItem>(
options: ParallelWorkerOptions<TItem>,
) {
const { parallelismFactor = 1, parallelismSetting, items, worker } = options;
const parallelism = parallelismSetting
? parseParallelismOption(parallelismSetting)
: getEnvironmentParallelism();
const sharedIterator = items[Symbol.iterator]();
const sharedIterable = {
[Symbol.iterator]: () => sharedIterator,
};
const workerCount = Math.max(Math.floor(parallelismFactor * parallelism), 1);
return Promise.all(
Array(workerCount)
.fill(0)
.map(async () => {
for (const value of sharedIterable) {
await worker(value);
}
}),
);
}
@@ -16,6 +16,7 @@
import React from 'react';
import { ErrorPage } from './ErrorPage';
import { Link } from '../../components/Link';
import { renderInTestApp } from '@backstage/test-utils';
describe('<ErrorPage/>', () => {
@@ -30,4 +31,72 @@ describe('<ErrorPage/>', () => {
).toBeInTheDocument();
expect(getByTestId('go-back-link')).toBeInTheDocument();
});
it('should render with additional information of type string', async () => {
const { getByText } = await renderInTestApp(
<ErrorPage
status="404"
statusMessage="PAGE NOT FOUND"
additionalInfo="This is a string based additional information"
/>,
);
expect(
getByText(/looks like someone dropped the mic!/i),
).toBeInTheDocument();
expect(
getByText(/This is a string based additional information/i),
).toBeInTheDocument();
});
it('should render with additional information including link', async () => {
const { getByText } = await renderInTestApp(
<ErrorPage
status="404"
statusMessage="PAGE NOT FOUND"
additionalInfo={
<>
This is some additional information including{' '}
<Link to="/test">a link</Link>
</>
}
/>,
);
expect(
getByText(/looks like someone dropped the mic!/i),
).toBeInTheDocument();
expect(getByText(/a link/i)).toBeInTheDocument();
expect(getByText(/a link/i)).toHaveAttribute('href', '/test');
});
it('should render with default support url if supportUrl is not provided', async () => {
const { getByText } = await renderInTestApp(
<ErrorPage status="404" statusMessage="PAGE NOT FOUND" />,
);
expect(
getByText(/looks like someone dropped the mic!/i),
).toBeInTheDocument();
expect(getByText(/contact support/i)).toBeInTheDocument();
expect(getByText(/contact support/i)).toHaveAttribute(
'href',
'https://github.com/backstage/backstage/issues',
);
});
it('should override support url if supportUrl property is provided', async () => {
const { getByText } = await renderInTestApp(
<ErrorPage
status="404"
statusMessage="PAGE NOT FOUND"
supportUrl="https://error-page-test-support-url.com"
/>,
);
expect(
getByText(/looks like someone dropped the mic!/i),
).toBeInTheDocument();
expect(getByText(/contact support/i)).toBeInTheDocument();
expect(getByText(/contact support/i)).toHaveAttribute(
'href',
'https://error-page-test-support-url.com',
);
});
});
@@ -27,7 +27,8 @@ import { MicDrop } from './MicDrop';
interface IErrorPageProps {
status: string;
statusMessage: string;
additionalInfo?: string;
additionalInfo?: React.ReactNode;
supportUrl?: string;
}
/** @public */
@@ -62,7 +63,7 @@ const useStyles = makeStyles<BackstageTheme>(
*
*/
export function ErrorPage(props: IErrorPageProps) {
const { status, statusMessage, additionalInfo } = props;
const { status, statusMessage, additionalInfo, supportUrl } = props;
const classes = useStyles();
const navigate = useNavigate();
const support = useSupportConfig();
@@ -88,7 +89,8 @@ export function ErrorPage(props: IErrorPageProps) {
<Link to="#" data-testid="go-back-link" onClick={() => navigate(-1)}>
Go back
</Link>
... or please <Link to={support.url}>contact support</Link> if you
... or please{' '}
<Link to={supportUrl || support.url}>contact support</Link> if you
think this is a bug.
</Typography>
</Grid>
@@ -14,7 +14,7 @@
"migrate:create": "knex migrate:make -x ts"
},
"dependencies": {
"app": "file:../app",
"app": "link:../app",
"@backstage/backend-common": "^{{version '@backstage/backend-common'}}",
"@backstage/backend-tasks": "^{{version '@backstage/backend-tasks'}}",
"@backstage/catalog-model": "^{{version '@backstage/catalog-model'}}",
@@ -1,12 +0,0 @@
# NOTE: This file is used for testing techdocs-cli locally
app:
title: Techdocs Preview App
baseUrl: http://localhost:3000
backend:
baseUrl: http://localhost:7007
techdocs:
builder: 'external'
requestUrl: http://localhost:7007/api
@@ -39,9 +39,8 @@
"start-server-and-test": "^1.10.11"
},
"scripts": {
"start": "backstage-cli app:serve --config ./app-config.yaml --config ./app-config.dev.yaml",
"start": "backstage-cli app:serve --config ./app-config.yaml",
"build": "backstage-cli app:build --config ./app-config.yaml",
"build:dev": "backstage-cli app:build --config ./app-config.dev.yaml",
"clean": "backstage-cli clean",
"test": "backstage-cli test",
"lint": "backstage-cli lint",
@@ -18,25 +18,24 @@ import React from 'react';
import { renderWithEffects } from '@backstage/test-utils';
import App from './App';
jest.mock('./config', () => ({
configLoader: async () => [
{
data: {
app: { title: 'Test' },
backend: { baseUrl: 'http://localhost:7007' },
techdocs: {
storageUrl: 'http://localhost:7007/api/techdocs/static/docs',
},
},
context: 'test',
},
],
}));
describe('App', () => {
it('should render', async () => {
process.env = {
NODE_ENV: 'test',
APP_CONFIG: [
{
data: {
app: { title: 'Test' },
backend: { baseUrl: 'http://localhost:7007' },
techdocs: {
storageUrl: 'http://localhost:7007/api/techdocs/static/docs',
},
},
context: 'test',
},
] as any,
};
const rendered = await renderWithEffects(<App />);
expect(rendered.baseElement).toBeInTheDocument();
expect(rendered.getByText('Docs Preview')).toBeInTheDocument();
});
});
@@ -29,9 +29,11 @@ import { apis } from './apis';
import { Root } from './components/Root';
import { techDocsPage } from './components/TechDocsPage';
import * as plugins from './plugins';
import { configLoader } from './config';
const app = createApp({
apis,
configLoader,
plugins: Object.values(plugins),
});
@@ -0,0 +1,59 @@
/*
* Copyright 2020 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 { defaultConfigLoader } from '@backstage/core-app-api';
const PRODUCTION_CONFIG = {
backend: {
baseUrl: 'http://localhost:3000',
},
techdocs: {
builder: 'external',
requestUrl: 'http://localhost:3000/api',
},
};
const DEVELOPMENT_CONFIG = {
backend: {
baseUrl: 'http://localhost:7007',
},
techdocs: {
builder: 'external',
requestUrl: 'http://localhost:7007/api',
},
};
async function isProductionServe() {
const res = await fetch('/.detect');
if (!res.ok) {
return false;
}
const text = await res.text();
return text.trim() === 'techdocs-cli-server';
}
export async function configLoader() {
const defaultConfigs = await defaultConfigLoader();
const isProduction = await isProductionServe();
return [
...defaultConfigs,
{
context: 'detected',
data: isProduction ? PRODUCTION_CONFIG : DEVELOPMENT_CONFIG,
},
];
}
+5 -3
View File
@@ -21,12 +21,13 @@
"types": "",
"scripts": {
"start": "nodemon --",
"build": "./scripts/build.sh",
"build": "backstage-cli build --outputs cjs",
"clean": "backstage-cli clean",
"lint": "backstage-cli lint",
"test": "backstage-cli test --testPathIgnorePatterns=src/e2e.test.ts",
"test:e2e": "backstage-cli test src/e2e.test.ts",
"test:e2e:ci": "backstage-cli test --watchAll=false --ci src/e2e.test.ts"
"test:e2e:ci": "backstage-cli test --watchAll=false --ci src/e2e.test.ts",
"prepack": "./scripts/prepack.sh"
},
"bin": {
"techdocs-cli": "bin/techdocs-cli"
@@ -40,7 +41,7 @@
"@types/node": "^14.14.32",
"@types/serve-handler": "^6.1.0",
"@types/webpack-env": "^1.15.3",
"techdocs-cli-embedded-app": "file:../techdocs-cli-embedded-app",
"techdocs-cli-embedded-app": "link:../techdocs-cli-embedded-app",
"find-process": "^1.4.5",
"nodemon": "^2.0.2",
"ts-node": "^10.0.0"
@@ -57,6 +58,7 @@
"dependencies": {
"@backstage/backend-common": "^0.10.6",
"@backstage/catalog-model": "^0.9.10",
"@backstage/cli-common": "^0.1.6",
"@backstage/config": "^0.1.13",
"@backstage/techdocs-common": "^0.11.6",
"@types/dockerode": "^3.3.0",
@@ -20,25 +20,7 @@ SCRIPT_DIR=$(dirname $0)
TECHDOCS_CLI_DIR="$SCRIPT_DIR"/..
TECHDOCS_CLI_EMBEDDED_APP_DIR="$TECHDOCS_CLI_DIR"/../techdocs-cli-embedded-app
compile_and_build_cli() {
echo "📄 Compiling..."
yarn workspace @techdocs/cli tsc > /dev/null
echo "📦️ Building..."
pushd $TECHDOCS_CLI_DIR > /dev/null
npx backstage-cli build --outputs cjs > /dev/null
popd > /dev/null
}
build_and_embed_app() {
echo "🚚 Embedding app..."
if [ "$TECHDOCS_CLI_DEV_MODE" = "true" ] ; then
yarn workspace techdocs-cli-embedded-app build:dev > /dev/null
else
yarn workspace techdocs-cli-embedded-app build > /dev/null
fi
cp -r "$TECHDOCS_CLI_EMBEDDED_APP_DIR"/dist "$TECHDOCS_CLI_DIR"/dist/techdocs-preview-bundle > /dev/null
}
compile_and_build_cli
build_and_embed_app
echo "🚚 Copying embedded app into dist/embedded-app"
rm -rf "$TECHDOCS_CLI_DIR"/dist/embedded-app
cp -r "$TECHDOCS_CLI_EMBEDDED_APP_DIR"/dist "$TECHDOCS_CLI_DIR"/dist/embedded-app
echo "🏁 Ready!"
@@ -17,11 +17,30 @@
import { Command } from 'commander';
import path from 'path';
import openBrowser from 'react-dev-utils/openBrowser';
import { findPaths } from '@backstage/cli-common';
import HTTPServer from '../../lib/httpServer';
import { runMkdocsServer } from '../../lib/mkdocsServer';
import { LogFunc, waitForSignal } from '../../lib/run';
import { createLogger } from '../../lib/utility';
function findPreviewBundlePath(): string {
try {
return path.join(
path.dirname(require.resolve('techdocs-cli-embedded-app/package.json')),
'dist',
);
} catch {
// If the techdocs-cli-embedded-app package is not available it means we're
// running a published package. For published packages the preview bundle is
// copied to dist/embedded-app be the prepack script.
//
// This can be tested by running `yarn pack` and extracting the resulting tarball into a directory.
// Within the extracted directory, run `npm install --only=prod`.
// Once that's done you can test the CLI in any directory using `node <tmp-dir>/package <command>`.
return findPaths(__dirname).resolveOwn('dist/embedded-app');
}
}
export default async function serve(cmd: Command) {
const logger = createLogger({ verbose: cmd.verbose });
@@ -91,16 +110,9 @@ export default async function serve(cmd: Command) {
);
}
// Run the embedded-techdocs Backstage app
const techdocsPreviewBundlePath = path.join(
path.dirname(require.resolve('@techdocs/cli/package.json')),
'dist',
'techdocs-preview-bundle',
);
const port = isDevMode ? backstageBackendPort : backstagePort;
const httpServer = new HTTPServer(
techdocsPreviewBundlePath,
findPreviewBundlePath(),
port,
cmd.mkdocsPort,
cmd.verbose,
+10 -2
View File
@@ -71,10 +71,18 @@ export default class HTTPServer {
response.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
request.url = forwardPath;
return proxy.web(request, response);
proxy.web(request, response);
return;
}
return serveHandler(request, response, {
// This endpoint is used by the frontend to detect where the backend is running.
if (request.url === '/.detect') {
response.setHeader('Content-Type', 'text/plain');
response.end('techdocs-cli-server');
return;
}
serveHandler(request, response, {
public: this.backstageBundleDir,
trailingSlash: true,
rewrites: [{ source: '**', destination: 'index.html' }],
+12 -1
View File
@@ -214,6 +214,17 @@ export class CatalogIdentityClient {
resolveCatalogMembership(query: MemberClaimQuery): Promise<string[]>;
}
// @public
export type CookieConfigurer = (ctx: {
providerId: string;
baseUrl: string;
callbackUrl: string;
}) => {
domain: string;
path: string;
secure: boolean;
};
// Warning: (ae-missing-release-tag) "createAtlassianProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -736,5 +747,5 @@ export type WebMessageResponse =
// src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts
// src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts
// src/providers/github/provider.d.ts:97:5 - (ae-forgotten-export) The symbol "StateEncoder" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:98:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
// src/providers/types.d.ts:118:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts
```
@@ -69,13 +69,14 @@ describe('OAuthAdapter', () => {
secure: false,
disableRefresh: true,
appOrigin: 'http://localhost:3000',
cookieDomain: 'localhost',
cookieDomain: 'example.com',
cookiePath: '/auth/test-provider',
tokenIssuer: {
issueToken: async () => 'my-id-token',
listPublicKeys: async () => ({ keys: [] }),
},
isOriginAllowed: () => false,
callbackUrl: 'http://example.com:7007/auth/test-provider/frame/handler',
};
it('sets the correct headers in start', async () => {
@@ -444,47 +445,6 @@ describe('OAuthAdapter', () => {
});
});
it('sets the correct cookie configuration using the base url', async () => {
const config = {
baseUrl: 'http://domain.org/auth',
appUrl: 'http://domain.org',
isOriginAllowed: () => false,
};
const oauthProvider = OAuthAdapter.fromConfig(
config,
providerInstance,
oAuthProviderOptions,
);
const mockRequest = {
query: {
scope: 'user',
env: 'development',
},
} as unknown as express.Request;
const mockResponse = {
cookie: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
statusCode: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.start(mockRequest, mockResponse);
expect(mockResponse.cookie).toBeCalledTimes(1);
expect(mockResponse.cookie).toBeCalledWith(
`${oAuthProviderOptions.providerId}-nonce`,
expect.any(String),
expect.objectContaining({
domain: 'domain.org',
path: '/auth/test-provider/handler',
secure: false,
}),
);
});
it('sets the correct cookie configuration using a callbackUrl', async () => {
const config = {
baseUrl: 'http://domain.org/auth',
@@ -35,7 +35,7 @@ import {
NotAllowedError,
} from '@backstage/errors';
import { TokenIssuer } from '../../identity/types';
import { getCookieConfig, readState, verifyNonce } from './helpers';
import { defaultCookieConfigurer, readState, verifyNonce } from './helpers';
import { postMessageResponse, ensuresXRequestedWith } from '../flow';
import {
OAuthHandlers,
@@ -58,7 +58,7 @@ export type Options = {
appOrigin: string;
tokenIssuer: TokenIssuer;
isOriginAllowed: (origin: string) => boolean;
callbackUrl?: string;
callbackUrl: string;
};
export class OAuthAdapter implements AuthProviderRouteHandlers {
static fromConfig(
@@ -74,18 +74,20 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
>,
): OAuthAdapter {
const { origin: appOrigin } = new URL(config.appUrl);
const authUrl = new URL(options.callbackUrl ?? config.baseUrl);
const { cookieDomain, cookiePath, secure } = getCookieConfig(
authUrl,
options.providerId,
);
const cookieConfigurer = config.cookieConfigurer ?? defaultCookieConfigurer;
const cookieConfig = cookieConfigurer({
providerId: options.providerId,
baseUrl: config.baseUrl,
callbackUrl: options.callbackUrl,
});
return new OAuthAdapter(handlers, {
...options,
appOrigin,
cookieDomain,
cookiePath,
secure,
cookieDomain: cookieConfig.domain,
cookiePath: cookieConfig.path,
secure: cookieConfig.secure,
isOriginAllowed: config.isOriginAllowed,
});
}
@@ -19,7 +19,7 @@ import {
verifyNonce,
encodeState,
readState,
getCookieConfig,
defaultCookieConfigurer,
} from './helpers';
describe('OAuthProvider Utils', () => {
@@ -110,30 +110,43 @@ describe('OAuthProvider Utils', () => {
});
});
describe('getCookieConfig', () => {
describe('defaultCookieConfigurer', () => {
it('should set the correct domain and path for a base url', () => {
const mockAuthUrl = new URL('http://domain.org/auth');
expect(getCookieConfig(mockAuthUrl, 'test-provider')).toMatchObject({
cookieDomain: 'domain.org',
cookiePath: '/auth/test-provider',
expect(
defaultCookieConfigurer({
baseUrl: '',
providerId: 'test-provider',
callbackUrl: 'http://domain.org/auth',
}),
).toMatchObject({
domain: 'domain.org',
path: '/auth/test-provider',
secure: false,
});
});
it('should set the correct domain and path for a url containing a frame handler', () => {
const mockAuthUrl = new URL(
'http://domain.org/auth/test-provider/handler/frame',
);
expect(getCookieConfig(mockAuthUrl, 'test-provider')).toMatchObject({
cookieDomain: 'domain.org',
cookiePath: '/auth/test-provider',
expect(
defaultCookieConfigurer({
baseUrl: '',
providerId: 'test-provider',
callbackUrl: 'http://domain.org/auth/test-provider/handler/frame',
}),
).toMatchObject({
domain: 'domain.org',
path: '/auth/test-provider',
secure: false,
});
});
it('should set the secure flag if url is using https', () => {
const mockAuthUrl = new URL('https://domain.org/auth');
expect(getCookieConfig(mockAuthUrl, 'test-provider')).toMatchObject({
expect(
defaultCookieConfigurer({
baseUrl: '',
providerId: 'test-provider',
callbackUrl: 'https://domain.org/auth',
}),
).toMatchObject({
secure: true,
});
});
@@ -17,6 +17,7 @@
import express from 'express';
import { OAuthState } from './types';
import pickBy from 'lodash/pickBy';
import { CookieConfigurer } from '../../providers/types';
export const readState = (stateString: string): OAuthState => {
const state = Object.fromEntries(
@@ -58,20 +59,19 @@ export const verifyNonce = (req: express.Request, providerId: string) => {
}
};
export const getCookieConfig = (authUrl: URL, providerId: string) => {
const { hostname: cookieDomain, pathname, protocol } = authUrl;
export const defaultCookieConfigurer: CookieConfigurer = ({
callbackUrl,
providerId,
}) => {
const { hostname: domain, pathname, protocol } = new URL(callbackUrl);
const secure = protocol === 'https:';
// If the provider supports callbackUrls, the pathname will
// contain the complete path to the frame handler so we need
// to slice off the trailing part of the path.
const cookiePath = pathname.endsWith(`${providerId}/handler/frame`)
const path = pathname.endsWith(`${providerId}/handler/frame`)
? pathname.slice(0, -'/handler/frame'.length)
: `${pathname}/${providerId}`;
return {
cookieDomain,
cookiePath,
secure,
};
return { domain, path, secure };
};
@@ -43,6 +43,7 @@ export type {
AuthHandlerResult,
SignInResolver,
SignInInfo,
CookieConfigurer,
} from './types';
// These types are needed for a postMessage from the login pop-up
@@ -38,6 +38,19 @@ export type AuthResolverContext = {
logger: Logger;
};
/**
* The callback used to resolve the cookie configuration for auth providers that use cookies.
* @public
*/
export type CookieConfigurer = (ctx: {
/** ID of the auth provider that this configuration applies to */
providerId: string;
/** The externally reachable base URL of the auth-backend plugin */
baseUrl: string;
/** The configured callback URL of the auth provider */
callbackUrl: string;
}) => { domain: string; path: string; secure: boolean };
export type AuthProviderConfig = {
/**
* The protocol://domain[:port] where the app is hosted. This is used to construct the
@@ -54,6 +67,11 @@ export type AuthProviderConfig = {
* A function that is called to check whether an origin is allowed to receive the authentication result.
*/
isOriginAllowed: (origin: string) => boolean;
/**
* The function used to resolve cookie configuration based on the auth provider options.
*/
cookieConfigurer?: CookieConfigurer;
};
export type RedirectInfo = {
+5 -1
View File
@@ -111,7 +111,11 @@ export async function createRouter(
try {
const provider = providerFactory({
providerId,
globalConfig: { baseUrl: authUrl, appUrl, isOriginAllowed },
globalConfig: {
baseUrl: authUrl,
appUrl,
isOriginAllowed,
},
config: providersConfig.getConfig(providerId),
logger,
tokenManager,
@@ -149,6 +149,7 @@ export type MicrosoftGraphProviderConfig = {
clientId: string;
clientSecret: string;
userFilter?: string;
userExpand?: string[];
userGroupMemberFilter?: string;
groupFilter?: string;
};
@@ -178,6 +179,7 @@ export function readMicrosoftGraphOrg(
client: MicrosoftGraphClient,
tenantId: string,
options: {
userExpand?: string[];
userFilter?: string;
userGroupMemberFilter?: string;
groupFilter?: string;
@@ -52,6 +52,12 @@ export type MicrosoftGraphProviderConfig = {
* E.g. "accountEnabled eq true and userType eq 'member'"
*/
userFilter?: string;
/**
* The expand argument to apply to users.
*
* E.g. "manager"
*/
userExpand?: string[];
/**
* The filter to apply to extract users by groups memberships.
*
@@ -85,6 +85,7 @@ export async function readMicrosoftGraphUsers(
client: MicrosoftGraphClient,
options: {
userFilter?: string;
userExpand?: string[];
transformer?: UserTransformer;
logger: Logger;
},
@@ -99,6 +100,7 @@ export async function readMicrosoftGraphUsers(
for await (const user of client.getUsers({
filter: options.userFilter,
expand: options.userExpand,
})) {
// Process all users in parallel, otherwise it can take quite some time
promises.push(
@@ -500,6 +502,7 @@ export async function readMicrosoftGraphOrg(
client: MicrosoftGraphClient,
tenantId: string,
options: {
userExpand?: string[];
userFilter?: string;
userGroupMemberFilter?: string;
groupFilter?: string;
@@ -524,6 +527,7 @@ export async function readMicrosoftGraphOrg(
} else {
const { users: usersWithFilter } = await readMicrosoftGraphUsers(client, {
userFilter: options.userFilter,
userExpand: options.userExpand,
transformer: options.userTransformer,
logger: options.logger,
});
@@ -102,6 +102,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor {
client,
provider.tenantId,
{
userExpand: provider.userExpand,
userFilter: provider.userFilter,
userGroupMemberFilter: provider.userGroupMemberFilter,
groupFilter: provider.groupFilter,
+13
View File
@@ -9,6 +9,19 @@ import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { RouteRef } from '@backstage/core-plugin-api';
// @public
export const DashboardSnapshotComponent: ({
guid,
name,
permalink,
duration,
}: {
guid: string;
name: string;
permalink: string;
duration: number;
}) => JSX.Element;
// Warning: (ae-missing-release-tag) "EntityNewRelicDashboardCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
+1
View File
@@ -17,5 +17,6 @@ export {
newRelicDashboardPlugin,
EntityNewRelicDashboardCard,
EntityNewRelicDashboardContent,
DashboardSnapshotComponent,
} from './plugin';
export { isNewRelicDashboardAvailable } from './Router';
+19
View File
@@ -60,3 +60,22 @@ export const EntityNewRelicDashboardCard = newRelicDashboardPlugin.provide(
},
}),
);
/**
* Render dashboard snapshots from Newrelic in backstage. Use dashboards which have the tag `isDashboardPage: true`
*
* @remarks
* This can be helpful for rendering dashboards outside of Entity Catalog.
*
* @public
*/
export const DashboardSnapshotComponent = newRelicDashboardPlugin.provide(
createComponentExtension({
name: 'DashboardSnapshotComponent',
component: {
lazy: () =>
import(
'./components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshot'
).then(m => m.DashboardSnapshot),
},
}),
);
@@ -21,6 +21,7 @@ import {
Lifecycle,
Page,
LogViewer,
Progress,
} from '@backstage/core-components';
import { useRouteRef } from '@backstage/core-plugin-api';
import { BackstageTheme } from '@backstage/theme';
@@ -352,6 +353,8 @@ export const TaskPage = ({ loadingText }: TaskPageProps) => {
</Paper>
</Grid>
<Grid item xs={9}>
{!currentStepId && <Progress />}
<div style={{ height: '80vh' }}>
<LogViewer text={logAsString} />
</div>
+44 -20
View File
@@ -25,31 +25,17 @@ const baseOptions = {
repo: 'backstage',
};
async function main() {
const { GITHUB_SHA, GITHUB_TOKEN } = process.env;
if (!GITHUB_SHA) {
throw new Error('GITHUB_SHA is not set');
}
if (!GITHUB_TOKEN) {
throw new Error('GITHUB_TOKEN is not set');
}
const octokit = new Octokit({ auth: GITHUB_TOKEN });
const rootPath = path.resolve(__dirname, '..');
const { version: currentVersion } = await fs.readJson(
path.join(rootPath, 'package.json'),
);
const tagName = `v${currentVersion}`;
console.log(`Creating release tag ${tagName}`);
async function getCurrentReleaseTag() {
const rootPath = path.resolve(__dirname, '../package.json');
return fs.readJson(rootPath).then(_ => _.version);
}
async function createGitTag(octokit, commitSha, tagName) {
const annotatedTag = await octokit.git.createTag({
...baseOptions,
tag: tagName,
message: tagName,
object: GITHUB_SHA,
object: commitSha,
type: 'commit',
});
@@ -69,8 +55,46 @@ async function main() {
console.error(`Tag creation for ${tagName} failed`);
throw ex;
}
}
async function dispatchReleaseWorkflows(octokit, releaseVersion) {
console.log('Dispatching release manifest sync');
await octokit.actions.createWorkflowDispatch({
owner: 'backstage',
repo: 'backstage',
workflow_id: 'sync_release-manifest.yml',
ref: 'master',
inputs: {
version: releaseVersion,
},
});
}
async function main() {
const shouldDispatch = process.argv.includes('--dispatch-workflows');
if (!process.env.GITHUB_SHA) {
throw new Error('GITHUB_SHA is not set');
}
if (!process.env.GITHUB_TOKEN) {
throw new Error('GITHUB_TOKEN is not set');
}
const commitSha = process.env.GITHUB_SHA;
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const releaseVersion = await getCurrentReleaseTag();
const tagName = `v${releaseVersion}`;
console.log(`Creating release tag ${tagName} at ${commitSha}`);
await createGitTag(octokit, commitSha, tagName);
console.log(`::set-output name=tag_name::${tagName}`);
if (shouldDispatch) {
console.log(`Dispatching release workflows for ${tagName}`);
await dispatchReleaseWorkflows(octokit, releaseVersion);
}
}
main().catch(error => {
+4 -4
View File
@@ -24,9 +24,9 @@
"storybook-dark-mode": "^1.0.8"
},
"peerDependencies": {
"@backstage/theme": "file:../packages/theme",
"@backstage/test-utils": "file:../packages/test-utils",
"@backstage/core-app-api": "file:../packages/core-app-api",
"@backstage/core-plugin-api": "file:../packages/core-plugin-api"
"@backstage/theme": "link:../packages/theme",
"@backstage/test-utils": "link:../packages/test-utils",
"@backstage/core-app-api": "link:../packages/core-app-api",
"@backstage/core-plugin-api": "link:../packages/core-plugin-api"
}
}
+174 -162
View File
@@ -311,7 +311,7 @@
dependencies:
"@babel/highlight" "^7.0.0"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.14.5", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3":
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3":
version "7.16.7"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789"
integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==
@@ -5761,6 +5761,11 @@
resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
"@types/npm-packlist@^1.1.2":
version "1.1.2"
resolved "https://registry.npmjs.org/@types/npm-packlist/-/npm-packlist-1.1.2.tgz#285978c9023ce68fa0641ca606c7c3b7b0e851c5"
integrity sha512-9NYoEH87t90e6dkaQOuUTY/R1xUE0a67sXzJBuAB+b+/z4FysHFD19g/O154ToGjyWqKYkezVUtuBdtfd4hyfw==
"@types/nunjucks@^3.1.4":
version "3.2.1"
resolved "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.2.1.tgz#02a3ade3dc4d3950029c6466a4034565dba7cf8c"
@@ -7300,10 +7305,10 @@ async@^2.6.2:
dependencies:
lodash "^4.17.14"
async@^3.1.0, async@^3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
async@^3.2.0, async@^3.2.3:
version "3.2.3"
resolved "https://registry.npmjs.org/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
asynckit@^0.4.0:
version "0.4.0"
@@ -8404,10 +8409,10 @@ check-types@^11.1.1:
resolved "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f"
integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ==
chokidar@^3.2.2, chokidar@^3.3.1, chokidar@^3.4.2, chokidar@^3.5.2:
version "3.5.2"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
chokidar@^3.2.2, chokidar@^3.3.1, chokidar@^3.4.2, chokidar@^3.5.2, chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
@@ -8792,7 +8797,7 @@ colors@1.0.3:
resolved "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
integrity sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=
colors@1.4.0, colors@^1.1.2, colors@^1.2.1:
colors@1.4.0, colors@^1.1.2:
version "1.4.0"
resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==
@@ -10885,65 +10890,65 @@ es6-weak-map@^2.0.3:
es6-iterator "^2.0.3"
es6-symbol "^3.1.1"
esbuild-android-arm64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.10.tgz#c854db57dc2d4df6f4f62185ca812f26a132bf1e"
integrity sha512-vzkTafHKoiMX4uIN1kBnE/HXYLpNT95EgGanVk6DHGeYgDolU0NBxjO7yZpq4ZGFPOx8384eAdDrBYhO11TAlQ==
esbuild-android-arm64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.20.tgz#7d1e7391030d83e2d6745ac297d630bb33130b36"
integrity sha512-MPKVDe3TMjGDRB5WmY9XnBaXEsPiiTpkz6GjXgBhBkMFZm27PhvZT4JE0vZ1fsLb5hnGC/fYsfAnp9rsxTZhIg==
esbuild-darwin-64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.10.tgz#c44fab6b8bfc83e5d083f513e4acbff14fb82eac"
integrity sha512-DJwzFVB95ZV7C3PQbf052WqaUuuMFXJeZJ0LKdnP1w+QOU0rlbKfX0tzuhoS//rOXUj1TFIwRuRsd0FX6skR7A==
esbuild-darwin-64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.20.tgz#b2633db8e87e36197965f18b6c0cfabc3497d8d2"
integrity sha512-09PPWejM3rRFsGHvtaTuRlG+KOQlOMwPW4HwwzRlO4TuP+FNV1nTW4x2Nid3dYLzCkcjznJWQ0oylLBQvGTRyQ==
esbuild-darwin-arm64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.10.tgz#9454b3763b36407dc395c4c3529fb5ddd4a6225f"
integrity sha512-RNaaoZDg3nsqs5z56vYCjk/VJ76npf752W0rOaCl5lO5TsgV9zecfdYgt7dtUrIx8b7APhVaNYud+tGsDOVC9g==
esbuild-darwin-arm64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.20.tgz#180fbebda4ec9376ffd8247a3d488f95c1d9df69"
integrity sha512-jYLrSXAwygoFF2lpRJSUAghre+9IThbcPvJQbcZMONBQaaZft9nclNsrN3k4u7zQaC8v+xZDVSHkmw593tQvkg==
esbuild-freebsd-64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.10.tgz#04eef46d5d5e4152c6b5a6a12f432db0fe7c89de"
integrity sha512-10B3AzW894u6bGZZhWiJOHw1uEHb4AFbUuBdyml1Ht0vIqd+KqWW+iY/yMwQAzILr2WJZqEhbOXRkJtY8aRqOw==
esbuild-freebsd-64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.20.tgz#4eb99ccf3e0b7ab039e5bbe491a44458991006c2"
integrity sha512-XShznPLW3QsK8/7iCx1euZTowWaWlcrlkq4YTlRqDKXkJRe98FJ6+V2QyoSTwwCoo5koaYwc+h/SYdglF5369A==
esbuild-freebsd-arm64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.10.tgz#67ca88529543ada948737c95819253ead16494a7"
integrity sha512-mSQrKB7UaWvuryBTCo9leOfY2uEUSimAvcKIaUWbk5Hth9Sg+Try+qNA/NibPgs/vHkX0KFo/Rce6RPea+P15g==
esbuild-freebsd-arm64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.20.tgz#5c6a02a4bc8ec8ff96c1142cf1509f1494aa78ff"
integrity sha512-flb3tDd6SScKhBqzWAESVCErpaqrGmMSRrssjx1aC+Ai5ZQrEyhfs5OWL4A9qHuixkhfmXffci7rFD+bNeXmZg==
esbuild-linux-32@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.10.tgz#8f3d5fb0b9b616d6b604da781d71767d7679f64f"
integrity sha512-lktF09JgJLZ63ANYHIPdYe339PDuVn19Q/FcGKkXWf+jSPkn5xkYzAabboNGZNUgNqSJ/vY7VrOn6UrBbJjgYA==
esbuild-linux-32@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.20.tgz#76af613e60a4f366d54d5d186c678bac36b18eda"
integrity sha512-Avtxbd0MHFJ2QhNxj/e8VGGm1/VnEJZq9qiHUl3wQZ4S0o2Wf4ReAfhqmgAbOPFTuxuZm070rRDZYiZifWzFGQ==
esbuild-linux-64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.10.tgz#c1c60a079c4709164bdd89fbb007a2edeea7c34a"
integrity sha512-K+gCQz2oLIIBI8ZM77e9sYD5/DwEpeYCrOQ2SYXx+R4OU2CT9QjJDi4/OpE7ko4AcYMlMW7qrOCuLSgAlEj4Wg==
esbuild-linux-64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.20.tgz#35d3c7d792403b913c308c92942c3f6893dc8225"
integrity sha512-ugisoRA/ajCr9JMszsQnT9hKkpbD7Gr1yl1mWdZhWQnGt6JKGIndGiihMURcrR44IK/2OMkixVe66D4gCHKdPA==
esbuild-linux-arm64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.10.tgz#d8f1f89190f6d8b6e06a1214aafba454e5daa990"
integrity sha512-+qocQuQvcp5wo/V+OLXxqHPc+gxHttJEvbU/xrCGE03vIMqraL4wMua8JQx0SWEnJCWP+Nhf//v8OSwz1Xr5kA==
esbuild-linux-arm64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.20.tgz#489e9187f95ce15e07e15a2aaadc53ec5ce1a02c"
integrity sha512-hsrMbNzhh+ud3zUyhONlR41vpYMjINS7BHEzXHbzo4YiCsG9Ht3arbiSuNGrhR/ybLr+8J/0fYVCipiVeAjy3Q==
esbuild-linux-arm@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.10.tgz#43192a00019a4553fb44e67f628fff0f560f16c2"
integrity sha512-BYa60dZ/KPmNKYxtHa3LSEdfKWHcm/RzP0MjB4AeBPhjS0D6/okhaBesZIY9kVIGDyeenKsJNOmnVt4+dhNnvQ==
esbuild-linux-arm@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.20.tgz#40c0f5aab33b8fe04e0528a6b8a073e9fb2ba6fd"
integrity sha512-uo++Mo31+P2EA38oQgOeSIWgD7GMCMpZkaLfsCqtKJTIIL9fVzQHQYLDRIiFGpLHvs1faWWHDCEcXEFSP1Ou0g==
esbuild-linux-mips64le@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.10.tgz#f57bb8b2f1a3063cc91cfd787c8a9130cf863c16"
integrity sha512-nmUd2xoBXpGo4NJCEWoaBj+n4EtDoLEvEYc8Z3aSJrY0Oa6s04czD1flmhd0I/d6QEU8b7GQ9U0g/rtBfhtxBg==
esbuild-linux-mips64le@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.20.tgz#3735a72ec09877b998f04c006af94f86575e4d7d"
integrity sha512-MBUu2Q+pzdTBWclPe7AwmRUMTUL0R99ONa8Hswpb987fXgFUdN4XBNBcEa5zy/l2UrIJK+9FUN1jjedZlxgP2A==
esbuild-linux-ppc64le@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.10.tgz#becd965bfe3425d41e026f1c4678b393127fecbd"
integrity sha512-vsOWZjm0rZix7HSmqwPph9arRVCyPtUpcURdayQDuIhMG2/UxJxpbdRaa//w4zYqcJzAWwuyH2PAlyy0ZNuxqQ==
esbuild-linux-ppc64le@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.20.tgz#bf58bb6e9d2bfb67a61c09297cf73c3a7116935d"
integrity sha512-xkYjQtITA6q/b+/5aAf5n2L063pOxLyXUIad+zYT8GpZh0Sa7aSn18BmrFa2fHb0QSGgTEeRfYkTcBGgoPDjBA==
esbuild-linux-s390x@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.10.tgz#cc4228ac842febc48b84757814bed964a619be62"
integrity sha512-knArKKZm0ypIYWOWyOT7+accVwbVV1LZnl2FWWy05u9Tyv5oqJ2F5+X2Vqe/gqd61enJXQWqoufXopvG3zULOg==
esbuild-linux-s390x@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.20.tgz#202699f42e5a7a77ebbf526953f6bbfb2cc68016"
integrity sha512-AAcj3x80TXIedpNVuZgjYNETXr2iciOBQv5pGdNGAy6rv7k6Y6sT6SXQ58l2LH2AHbaeTPQjze+Y6qgX1efzrA==
esbuild-loader@^2.18.0:
version "2.18.0"
@@ -10957,59 +10962,59 @@ esbuild-loader@^2.18.0:
tapable "^2.2.0"
webpack-sources "^2.2.0"
esbuild-netbsd-64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.10.tgz#6ec50d9e4547a7579f447307b19f66bbedfd868b"
integrity sha512-6Gg8neVcLeyq0yt9bZpReb8ntZ8LBEjthxrcYWVrBElcltnDjIy1hrzsujt0+sC2rL+TlSsE9dzgyuvlDdPp2w==
esbuild-netbsd-64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.20.tgz#fb133b9726b8e672a7df57629fdc71606952d37c"
integrity sha512-30GQKCnsID1WddUi6tr5HFUxJD0t7Uitf6tO9Cf1WqF6C44pf8EflwrhyDFmUyvkddlyfb4OrYI6NNLC/G3ajg==
esbuild-openbsd-64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.10.tgz#925ac3d2326cc219d514e1ca806e80e5143aa043"
integrity sha512-9rkHZzp10zI90CfKbFrwmQjqZaeDmyQ6s9/hvCwRkbOCHuto6RvMYH9ghQpcr5cUxD5OQIA+sHXi0zokRNXjcg==
esbuild-openbsd-64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.20.tgz#50e879a09bb465cda8c9a2f03ba5c2096848c7a1"
integrity sha512-zVrf8fY46BK57AkxDdqu2S8TV3p7oLmYIiW707IOHrveI0TwJ2iypAxnwOQuCvowM3UWqVBO2RDBzV7S7t0klg==
esbuild-sunos-64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.10.tgz#8d3576d8cac5c4f9f2a84be81b9078d424dbc739"
integrity sha512-mEU+pqkhkhbwpJj5DiN3vL0GUFR/yrL3qj8ER1amIVyRibKbj02VM1QaIuk1sy5DRVIKiFXXgCaHvH3RNWCHIw==
esbuild-sunos-64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.20.tgz#cb1c55c86513226296935a9bc97fe9457b2a2de4"
integrity sha512-MYRsS1O7+aBr2T/0aA4OJrju6eMku4rm81fwGF1KLFwmymIpPGmj7n69n5JW3NKyW5j+FBt0GcyDh9nEnUL1FQ==
esbuild-windows-32@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.10.tgz#8a67fca4cb594a340566d66eef3f568f65057a48"
integrity sha512-Z5DieUL1N6s78dOSdL95KWf8Y89RtPGxIoMF+LEy8ChDsX+pZpz6uAVCn+YaWpqQXO+2TnrcbgBIoprq2Mco1g==
esbuild-windows-32@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.20.tgz#5e4db2758408e148e225f06c7724853386916c70"
integrity sha512-7VqDITqTU65LQ1Uka/4jx4sUIZc1L8NPlvc7HBRdR15TUyPxmHRQaxMGXd8aakI1FEBcImpJ9SQ4JLmPwRlS1w==
esbuild-windows-64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.10.tgz#5e6d7c475ff6a71ad0aa4046894364e6c40a9249"
integrity sha512-LE5Mm62y0Bilu7RDryBhHIX8rK3at5VwJ6IGM3BsASidCfOBTzqcs7Yy0/Vkq39VKeTmy9/66BAfVoZRNznoDw==
esbuild-windows-64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.20.tgz#0731564e8396091b2ac487fb266c86a2bdd45b37"
integrity sha512-q4GxY4m5+nXSgqCKx6Cc5pavnhd2g5mHn+K8kNdfCMZsWPDlHLMRjYF5NVQ3/5mJ1M7iR3/Ai4ISjxmsCeGOGA==
esbuild-windows-arm64@0.14.10:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.10.tgz#50ab9a83f6ccf71c272e58489ecc4d7375075f32"
integrity sha512-OJOyxDtabvcUYTc+O4dR0JMzLBz6G9+gXIHA7Oc5d5Fv1xiYa0nUeo8+W5s2e6ZkPRdIwOseYoL70rZz80S5BA==
esbuild-windows-arm64@0.14.20:
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.20.tgz#615978735d3a8b5d0a8e4c35d5a18c0733920d4d"
integrity sha512-vOxfU7YwuBMjsUNUygMBhC8T60aCzeYptnHu4k7azqqOVo5EAyoueyWSkFR5GpX6bae5cXyB0vcOV/bfwqRwAg==
esbuild@^0.14.1, esbuild@^0.14.10, esbuild@^0.14.6:
version "0.14.10"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.10.tgz#10268d2b576b25ed6f8554553413988628a7767b"
integrity sha512-ibZb+NwFqBwHHJlpnFMtg4aNmVK+LUtYMFC9CuKs6lDCBEvCHpqCFZFEirpqt1jOugwKGx8gALNGvX56lQyfew==
version "0.14.20"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.20.tgz#e83fcb838463f220e864141752bb0f91bfc9c33a"
integrity sha512-7aRJRnTjHZ6rFEre52tsAYZxatVELSA/QvYGUBf1iOsYKCnSJICE5seugQFFJgV1Gyl0/mngxQPhxBIqgYG2BA==
optionalDependencies:
esbuild-android-arm64 "0.14.10"
esbuild-darwin-64 "0.14.10"
esbuild-darwin-arm64 "0.14.10"
esbuild-freebsd-64 "0.14.10"
esbuild-freebsd-arm64 "0.14.10"
esbuild-linux-32 "0.14.10"
esbuild-linux-64 "0.14.10"
esbuild-linux-arm "0.14.10"
esbuild-linux-arm64 "0.14.10"
esbuild-linux-mips64le "0.14.10"
esbuild-linux-ppc64le "0.14.10"
esbuild-linux-s390x "0.14.10"
esbuild-netbsd-64 "0.14.10"
esbuild-openbsd-64 "0.14.10"
esbuild-sunos-64 "0.14.10"
esbuild-windows-32 "0.14.10"
esbuild-windows-64 "0.14.10"
esbuild-windows-arm64 "0.14.10"
esbuild-android-arm64 "0.14.20"
esbuild-darwin-64 "0.14.20"
esbuild-darwin-arm64 "0.14.20"
esbuild-freebsd-64 "0.14.20"
esbuild-freebsd-arm64 "0.14.20"
esbuild-linux-32 "0.14.20"
esbuild-linux-64 "0.14.20"
esbuild-linux-arm "0.14.20"
esbuild-linux-arm64 "0.14.20"
esbuild-linux-mips64le "0.14.20"
esbuild-linux-ppc64le "0.14.20"
esbuild-linux-s390x "0.14.20"
esbuild-netbsd-64 "0.14.20"
esbuild-openbsd-64 "0.14.20"
esbuild-sunos-64 "0.14.20"
esbuild-windows-32 "0.14.20"
esbuild-windows-64 "0.14.20"
esbuild-windows-arm64 "0.14.20"
escalade@^3.1.1:
version "3.1.1"
@@ -11442,7 +11447,7 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
md5.js "^1.3.4"
safe-buffer "^5.1.1"
"example-app@file:packages/app":
"example-app@link:packages/app":
version "0.2.63"
dependencies:
"@backstage/app-defaults" "^0.1.6"
@@ -12166,22 +12171,21 @@ fork-ts-checker-webpack-plugin@^6.5.0:
tapable "^1.0.0"
fork-ts-checker-webpack-plugin@^7.0.0-alpha.8:
version "7.0.0-alpha.11"
resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.0.0-alpha.11.tgz#aa51ff15f203547ae6052b91fa912371db7557b9"
integrity sha512-10Q0sjG24BqIkAEFCb+JP0laM6gYO2+3ZV0lBHQ6kJ0+Ot2TffRFcyNkWQBRQqoyqtrDaHfxnjHJ+uXsMO13Bg==
version "7.1.1"
resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.1.1.tgz#9806c04f3aecaec8c9e8872806cd1f26d50d92a8"
integrity sha512-MgAv1g6+HLXz1iq2AZRfBjDhwBwk1HRSjuhNiMs8ax/3tsFo0rHedKwuE6ok44sJ5F5VgwPZae8gq0wiBAqYRw==
dependencies:
"@babel/code-frame" "^7.14.5"
"@babel/code-frame" "^7.16.7"
chalk "^4.1.2"
chokidar "^3.5.2"
cosmiconfig "^7.0.0"
chokidar "^3.5.3"
cosmiconfig "^7.0.1"
deepmerge "^4.2.2"
fs-extra "^10.0.0"
glob "^7.1.7"
memfs "^3.2.2"
memfs "^3.4.1"
minimatch "^3.0.4"
schema-utils "3.1.1"
schema-utils "4.0.0"
semver "^7.3.5"
tapable "^2.0.0"
tapable "^2.2.1"
form-data-encoder@^1.4.3:
version "1.6.0"
@@ -13546,6 +13550,13 @@ ignore-walk@^3.0.1, ignore-walk@^3.0.3:
dependencies:
minimatch "^3.0.4"
ignore-walk@^4.0.1:
version "4.0.1"
resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-4.0.1.tgz#fc840e8346cf88a3a9380c5b17933cd8f4d39fa3"
integrity sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==
dependencies:
minimatch "^3.0.4"
ignore@^3.3.5:
version "3.3.10"
resolved "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
@@ -16239,17 +16250,6 @@ log-update@^4.0.0:
slice-ansi "^4.0.0"
wrap-ansi "^6.2.0"
logform@^2.2.0:
version "2.3.0"
resolved "https://registry.npmjs.org/logform/-/logform-2.3.0.tgz#a3997a05985de2ebd325ae0d166dffc9c6fe6b57"
integrity sha512-graeoWUH2knKbGthMtuG1EfaSPMZFZBIrhuJHhkS5ZseFBrc7DupCzihOQAzsK/qIKPQaPJ/lFQFctILUY5ARQ==
dependencies:
colors "^1.2.1"
fecha "^4.2.0"
ms "^2.1.1"
safe-stable-stringify "^1.1.0"
triple-beam "^1.3.0"
logform@^2.3.2:
version "2.3.2"
resolved "https://registry.npmjs.org/logform/-/logform-2.3.2.tgz#68babe6a74ab09a1fd15a9b1e6cbc7713d41cb5b"
@@ -16701,10 +16701,10 @@ media-typer@0.3.0:
vinyl "^2.0.1"
vinyl-file "^3.0.0"
memfs@^3.1.2, memfs@^3.2.2:
version "3.2.2"
resolved "https://registry.npmjs.org/memfs/-/memfs-3.2.2.tgz#5de461389d596e3f23d48bb7c2afb6161f4df40e"
integrity sha512-RE0CwmIM3CEvpcdK3rZ19BC4E6hv9kADkMN5rPduRak58cNArWLi/9jFLsa4rhsjfVxMP3v0jO7FHXq7SvFY5Q==
memfs@^3.1.2, memfs@^3.2.2, memfs@^3.4.1:
version "3.4.1"
resolved "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz#b78092f466a0dce054d63d39275b24c71d3f1305"
integrity sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==
dependencies:
fs-monkey "1.0.3"
@@ -18013,6 +18013,16 @@ npm-packlist@^2.1.4:
npm-bundled "^1.1.1"
npm-normalize-package-bin "^1.0.1"
npm-packlist@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-3.0.0.tgz#0370df5cfc2fcc8f79b8f42b37798dd9ee32c2a9"
integrity sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ==
dependencies:
glob "^7.1.6"
ignore-walk "^4.0.1"
npm-bundled "^1.1.1"
npm-normalize-package-bin "^1.0.1"
npm-pick-manifest@^6.0.0:
version "6.1.0"
resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a"
@@ -20561,7 +20571,7 @@ readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stre
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@^2.3.7, readable-stream@~2.3.6:
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
@@ -21386,7 +21396,7 @@ safe-stable-stringify@^1.1.0:
resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz#c8a220ab525cd94e60ebf47ddc404d610dc5d84a"
integrity sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw==
safe-stable-stringify@^2.2.0:
safe-stable-stringify@^2.2.0, safe-stable-stringify@^2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73"
integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg==
@@ -21453,16 +21463,7 @@ schema-utils@2.7.0:
ajv "^6.12.2"
ajv-keywords "^3.4.1"
schema-utils@3.1.1, schema-utils@^3.1.0, schema-utils@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==
dependencies:
"@types/json-schema" "^7.0.8"
ajv "^6.12.5"
ajv-keywords "^3.5.2"
schema-utils@^4.0.0:
schema-utils@4.0.0, schema-utils@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz#60331e9e3ae78ec5d16353c467c34b3a0a1d3df7"
integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==
@@ -21472,6 +21473,15 @@ schema-utils@^4.0.0:
ajv-formats "^2.1.1"
ajv-keywords "^5.0.0"
schema-utils@^3.1.0, schema-utils@^3.1.1:
version "3.1.1"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281"
integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==
dependencies:
"@types/json-schema" "^7.0.8"
ajv "^6.12.5"
ajv-keywords "^3.5.2"
scoped-regex@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/scoped-regex/-/scoped-regex-2.1.0.tgz#7b9be845d81fd9d21d1ec97c61a0b7cf86d2015f"
@@ -22885,10 +22895,10 @@ tapable@^1.0.0:
resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0:
version "2.2.0"
resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.0.tgz#5c373d281d9c672848213d0e037d1c4165ab426b"
integrity sha512-FBk4IesMV1rBxX2tfiK8RAmogtWn53puLOQlvO8XuwlgxcYbP4mVPS9Ph4aeamSyyVjOl24aYWAuc8U5kCVwMw==
tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1:
version "2.2.1"
resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
tar-fs@2.1.1, tar-fs@^2.1.1:
version "2.1.1"
@@ -22967,7 +22977,7 @@ tdigest@^0.1.1:
dependencies:
bintrees "1.0.1"
"techdocs-cli-embedded-app@file:packages/techdocs-cli-embedded-app":
"techdocs-cli-embedded-app@link:packages/techdocs-cli-embedded-app":
version "0.2.62"
dependencies:
"@backstage/app-defaults" "^0.1.6"
@@ -23404,7 +23414,7 @@ trim-off-newlines@^1.0.0:
resolved "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.3.tgz#8df24847fcb821b0ab27d58ab6efec9f2fe961a1"
integrity sha512-kh6Tu6GbeSNMGfrrZh6Bb/4ZEHV1QlB4xNDBeog8Y9/QwFlKTRyWvY3Fs9tRDAMZliVUwieMgEdIeL/FtqjkJg==
triple-beam@^1.2.0, triple-beam@^1.3.0:
triple-beam@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9"
integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
@@ -23657,9 +23667,9 @@ typescript@~4.4.4:
integrity sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==
typescript@~4.5.2, typescript@~4.5.4:
version "4.5.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8"
integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==
version "4.5.5"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3"
integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==
ua-parser-js@^0.7.18:
version "0.7.28"
@@ -24642,28 +24652,30 @@ windows-release@^3.1.0:
dependencies:
execa "^1.0.0"
winston-transport@^4.4.0:
version "4.4.0"
resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz#17af518daa690d5b2ecccaa7acf7b20ca7925e59"
integrity sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==
winston-transport@^4.4.2:
version "4.5.0"
resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa"
integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==
dependencies:
readable-stream "^2.3.7"
triple-beam "^1.2.0"
logform "^2.3.2"
readable-stream "^3.6.0"
triple-beam "^1.3.0"
winston@^3.2.1:
version "3.3.3"
resolved "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170"
integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==
version "3.5.1"
resolved "https://registry.npmjs.org/winston/-/winston-3.5.1.tgz#b25cc899d015836dbf8c583dec8c4c4483a0da2e"
integrity sha512-tbRtVy+vsSSCLcZq/8nXZaOie/S2tPXPFt4be/Q3vI/WtYwm7rrwidxVw2GRa38FIXcJ1kUM6MOZ9Jmnk3F3UA==
dependencies:
"@dabh/diagnostics" "^2.0.2"
async "^3.1.0"
async "^3.2.3"
is-stream "^2.0.0"
logform "^2.2.0"
logform "^2.3.2"
one-time "^1.0.0"
readable-stream "^3.4.0"
safe-stable-stringify "^2.3.1"
stack-trace "0.0.x"
triple-beam "^1.3.0"
winston-transport "^4.4.0"
winston-transport "^4.4.2"
word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3"