diff --git a/.changeset/breezy-windows-jump.md b/.changeset/breezy-windows-jump.md new file mode 100644 index 0000000000..0d839614ff --- /dev/null +++ b/.changeset/breezy-windows-jump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': minor +--- + +The `callbackUrl` option of `OAuthAdapter` is now required. diff --git a/.changeset/cool-birds-ring.md b/.changeset/cool-birds-ring.md new file mode 100644 index 0000000000..0ecd4a47e9 --- /dev/null +++ b/.changeset/cool-birds-ring.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-newrelic-dashboard': patch +--- + +Export DashboardSnapshotComponent from new-relic-dashboard-plugin diff --git a/.changeset/healthy-flies-fold.md b/.changeset/healthy-flies-fold.md new file mode 100644 index 0000000000..ae42533f8a --- /dev/null +++ b/.changeset/healthy-flies-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Removed unnecessary `get-port` dependency diff --git a/.changeset/khaki-jokes-grab.md b/.changeset/khaki-jokes-grab.md new file mode 100644 index 0000000000..31201c1078 --- /dev/null +++ b/.changeset/khaki-jokes-grab.md @@ -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. diff --git a/.changeset/loud-monkeys-explode.md b/.changeset/loud-monkeys-explode.md new file mode 100644 index 0000000000..b17b1e1641 --- /dev/null +++ b/.changeset/loud-monkeys-explode.md @@ -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. diff --git a/.changeset/many-terms-type.md b/.changeset/many-terms-type.md new file mode 100644 index 0000000000..fd11d14106 --- /dev/null +++ b/.changeset/many-terms-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +The experimental types build enabled by `--experimental-type-build` now runs in a separate worker thread. diff --git a/.changeset/metal-clouds-fail.md b/.changeset/metal-clouds-fail.md new file mode 100644 index 0000000000..083984c015 --- /dev/null +++ b/.changeset/metal-clouds-fail.md @@ -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. diff --git a/.changeset/metal-lions-fix.md b/.changeset/metal-lions-fix.md new file mode 100644 index 0000000000..913098bbf7 --- /dev/null +++ b/.changeset/metal-lions-fix.md @@ -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. diff --git a/.changeset/nasty-socks-exist.md b/.changeset/nasty-socks-exist.md new file mode 100644 index 0000000000..6aa3e56a1d --- /dev/null +++ b/.changeset/nasty-socks-exist.md @@ -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'}}", +``` diff --git a/.changeset/ninety-dancers-bow.md b/.changeset/ninety-dancers-bow.md new file mode 100644 index 0000000000..dc8bd7b702 --- /dev/null +++ b/.changeset/ninety-dancers-bow.md @@ -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 diff --git a/.changeset/pretty-glasses-admire.md b/.changeset/pretty-glasses-admire.md new file mode 100644 index 0000000000..318271ddc9 --- /dev/null +++ b/.changeset/pretty-glasses-admire.md @@ -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' +``` diff --git a/.changeset/tasty-spoons-beg.md b/.changeset/tasty-spoons-beg.md new file mode 100644 index 0000000000..651c70e03c --- /dev/null +++ b/.changeset/tasty-spoons-beg.md @@ -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. diff --git a/.changeset/three-dolls-fly.md b/.changeset/three-dolls-fly.md new file mode 100644 index 0000000000..df711c547d --- /dev/null +++ b/.changeset/three-dolls-fly.md @@ -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. diff --git a/.changeset/twenty-colts-applaud.md b/.changeset/twenty-colts-applaud.md new file mode 100644 index 0000000000..2390e21a6e --- /dev/null +++ b/.changeset/twenty-colts-applaud.md @@ -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. diff --git a/.changeset/warm-beds-flow.md b/.changeset/warm-beds-flow.md new file mode 100644 index 0000000000..7d34336739 --- /dev/null +++ b/.changeset/warm-beds-flow.md @@ -0,0 +1,5 @@ +--- +'@techdocs/cli': patch +--- + +Updated the HTTP server to allow for simplification of the development of the CLI itself. diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index 459f48336e..ed02a313fe 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -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 }} diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index 39e270af16..85c01d0631 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -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, + }, + }); diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index ec27a81653..383c3ae6d2 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -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. diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index b661241623..38c74f021f 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -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", diff --git a/packages/backend/package.json b/packages/backend/package.json index b61b7ad81c..5fd5a5d33c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -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", diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js index a97f26f8da..519b388b96 100644 --- a/packages/cli/config/eslint.backend.js +++ b/packages/cli/config/eslint.backend.js @@ -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], }, }, diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index 781af13577..06f22933e8 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -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, - }, - ], - }, - }, ], }; diff --git a/packages/cli/package.json b/packages/cli/package.json index 41cefe6c53..0f6c521ec4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/commands/app/build.ts b/packages/cli/src/commands/app/build.ts index 36d910fe5b..67b71632c7 100644 --- a/packages/cli/src/commands/app/build.ts +++ b/packages/cli/src/commands/app/build.ts @@ -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, diff --git a/packages/cli/src/commands/backend/bundle.ts b/packages/cli/src/commands/backend/bundle.ts index 338d5cdaa0..ec5fa0a131 100644 --- a/packages/cli/src/commands/backend/bundle.ts +++ b/packages/cli/src/commands/backend/bundle.ts @@ -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, }); diff --git a/packages/cli/src/commands/build/buildApp.ts b/packages/cli/src/commands/build/buildApp.ts index 3d86d796b7..b0959b2d6a 100644 --- a/packages/cli/src/commands/build/buildApp.ts +++ b/packages/cli/src/commands/build/buildApp.ts @@ -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, })), }); diff --git a/packages/cli/src/commands/build/buildBackend.ts b/packages/cli/src/commands/build/buildBackend.ts index a4d8858cf8..e32b7956db 100644 --- a/packages/cli/src/commands/build/buildBackend.ts +++ b/packages/cli/src/commands/build/buildBackend.ts @@ -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, }); diff --git a/packages/cli/src/commands/build/command.ts b/packages/cli/src/commands/build/command.ts index 8c13b515d7..1189883b12 100644 --- a/packages/cli/src/commands/build/command.ts +++ b/packages/cli/src/commands/build/command.ts @@ -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 { 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), }); } diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index a33e7d4afe..2211af1dda 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -25,6 +25,25 @@ const configOption = [ Array(), ] 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); diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 5c3e0a88e3..823d24fbe8 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -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); + } }; diff --git a/packages/cli/src/commands/repo/build.ts b/packages/cli/src/commands/repo/build.ts new file mode 100644 index 0000000000..9c32386304 --- /dev/null +++ b/packages/cli/src/commands/repo/build.ts @@ -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 = {}; + 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 { + const packages = await PackageGraph.listTargetPackages(); + const apps = new Array(); + const backends = new Array(); + + 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, + }); + }, + }); + } +} diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 2dadd06d1d..ae1a4c4627 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -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!', ]); diff --git a/packages/cli/src/commands/versions/bump.ts b/packages/cli/src/commands/versions/bump.ts index 2c16f324bc..0d7c1cdfae 100644 --- a/packages/cli/src/commands/versions/bump.ts +++ b/packages/cli/src/commands/versions/bump.ts @@ -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(); // 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(); - 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( - count: number, - items: IterableIterator, - fn: (item: T) => Promise, -) { - 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()), - ); -} diff --git a/packages/cli/src/lib/builder/buildTypeDefinitions.ts b/packages/cli/src/lib/builder/buildTypeDefinitions.ts index ba67532bdb..85e54850e4 100644 --- a/packages/cli/src/lib/builder/buildTypeDefinitions.ts +++ b/packages/cli/src/lib/builder/buildTypeDefinitions.ts @@ -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((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); + } + } + }); + }); } diff --git a/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts new file mode 100644 index 0000000000..8cb843f58e --- /dev/null +++ b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts @@ -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' }); + } +} diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts index 617e354864..0a214f1a7a 100644 --- a/packages/cli/src/lib/builder/config.ts +++ b/packages/cli/src/lib/builder/config.ts @@ -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 { const configs = new Array(); + 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(); @@ -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()], }); } diff --git a/packages/cli/src/lib/builder/index.ts b/packages/cli/src/lib/builder/index.ts index c39d964ade..00cc463cfb 100644 --- a/packages/cli/src/lib/builder/index.ts +++ b/packages/cli/src/lib/builder/index.ts @@ -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'; diff --git a/packages/cli/src/lib/builder/packager.ts b/packages/cli/src/lib/builder/packager.ts index 96c488fce2..c2256c8417 100644 --- a/packages/cli/src/lib/builder/packager.ts +++ b/packages/cli/src/lib/builder/packager.ts @@ -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 { + const outputs = new Set(); + + 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; +} diff --git a/packages/cli/src/lib/builder/types.ts b/packages/cli/src/lib/builder/types.ts index a02afef01a..f43d84cd92 100644 --- a/packages/cli/src/lib/builder/types.ts +++ b/packages/cli/src/lib/builder/types.ts @@ -21,6 +21,8 @@ export enum Output { } export type BuildOptions = { + logPrefix?: string; + targetDir?: string; outputs: Set; minify?: boolean; useApiExtractor?: boolean; diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index bd2d27a2b8..3cfcafc005 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -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, diff --git a/packages/cli/src/lib/bundler/paths.ts b/packages/cli/src/lib/bundler/paths.ts index d465b3010f..3d4925c66b 100644 --- a/packages/cli/src/lib/bundler/paths.ts +++ b/packages/cli/src/lib/bundler/paths.ts @@ -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, }; diff --git a/packages/cli/src/lib/bundler/types.ts b/packages/cli/src/lib/bundler/types.ts index eae0452d8d..4d6d2e5c9e 100644 --- a/packages/cli/src/lib/bundler/types.ts +++ b/packages/cli/src/lib/bundler/types.ts @@ -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; }; diff --git a/packages/cli/src/lib/packager/copyPackageDist.ts b/packages/cli/src/lib/packager/copyPackageDist.ts new file mode 100644 index 0000000000..03ac361006 --- /dev/null +++ b/packages/cli/src/lib/packager/copyPackageDist.ts @@ -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'); +} diff --git a/packages/cli/src/lib/packager/createDistWorkspace.ts b/packages/cli/src/lib/packager/createDistWorkspace.ts new file mode 100644 index 0000000000..15ccda30c2 --- /dev/null +++ b/packages/cli/src/lib/packager/createDistWorkspace.ts @@ -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(); + const customBuild = new Array(); + + 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 { + 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`), + ), + ); +} diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index c35e2587b9..75f3fdf71d 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -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 { - 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'; diff --git a/packages/cli/src/lib/parallel.test.ts b/packages/cli/src/lib/parallel.test.ts index c5c33473ff..d851f33f82 100644 --- a/packages/cli/src/lib/parallel.test.ts +++ b/packages/cli/src/lib/parallel.test.ts @@ -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(); + const done = new Array(); + 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(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(); + const done = new Array(); + 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(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]); }); }); diff --git a/packages/cli/src/lib/parallel.ts b/packages/cli/src/lib/parallel.ts index f922295a89..f8af967d22 100644 --- a/packages/cli/src/lib/parallel.ts +++ b/packages/cli/src/lib/parallel.ts @@ -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 = { + /** + * 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; + worker: (item: TItem) => Promise; +}; + +export async function runParallelWorkers( + options: ParallelWorkerOptions, +) { + 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); + } + }), + ); +} diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx index a314128bce..2efdc5da33 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.test.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { ErrorPage } from './ErrorPage'; +import { Link } from '../../components/Link'; import { renderInTestApp } from '@backstage/test-utils'; describe('', () => { @@ -30,4 +31,72 @@ describe('', () => { ).toBeInTheDocument(); expect(getByTestId('go-back-link')).toBeInTheDocument(); }); + + it('should render with additional information of type string', async () => { + const { getByText } = await renderInTestApp( + , + ); + 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( + + This is some additional information including{' '} + a 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( + , + ); + 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( + , + ); + 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', + ); + }); }); diff --git a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx index 08c002d849..928a3eb0d4 100644 --- a/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core-components/src/layout/ErrorPage/ErrorPage.tsx @@ -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( * */ 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) { navigate(-1)}> Go back - ... or please contact support if you + ... or please{' '} + contact support if you think this is a bug. diff --git a/packages/create-app/templates/default-app/packages/backend/package.json.hbs b/packages/create-app/templates/default-app/packages/backend/package.json.hbs index 2288b475a9..b27d420820 100644 --- a/packages/create-app/templates/default-app/packages/backend/package.json.hbs +++ b/packages/create-app/templates/default-app/packages/backend/package.json.hbs @@ -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'}}", diff --git a/packages/techdocs-cli-embedded-app/app-config.dev.yaml b/packages/techdocs-cli-embedded-app/app-config.dev.yaml deleted file mode 100644 index 02d68c940e..0000000000 --- a/packages/techdocs-cli-embedded-app/app-config.dev.yaml +++ /dev/null @@ -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 diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 6eb3119b06..2df050ef86 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -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", diff --git a/packages/techdocs-cli-embedded-app/src/App.test.tsx b/packages/techdocs-cli-embedded-app/src/App.test.tsx index f177433d95..75658d271e 100644 --- a/packages/techdocs-cli-embedded-app/src/App.test.tsx +++ b/packages/techdocs-cli-embedded-app/src/App.test.tsx @@ -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(); - expect(rendered.baseElement).toBeInTheDocument(); + expect(rendered.getByText('Docs Preview')).toBeInTheDocument(); }); }); diff --git a/packages/techdocs-cli-embedded-app/src/App.tsx b/packages/techdocs-cli-embedded-app/src/App.tsx index 00b232d8e1..51bdfbbb11 100644 --- a/packages/techdocs-cli-embedded-app/src/App.tsx +++ b/packages/techdocs-cli-embedded-app/src/App.tsx @@ -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), }); diff --git a/packages/techdocs-cli-embedded-app/src/config.ts b/packages/techdocs-cli-embedded-app/src/config.ts new file mode 100644 index 0000000000..482ceb41ec --- /dev/null +++ b/packages/techdocs-cli-embedded-app/src/config.ts @@ -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, + }, + ]; +} diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index a8dab88c55..2076e25bed 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -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", diff --git a/packages/techdocs-cli/scripts/build.sh b/packages/techdocs-cli/scripts/prepack.sh similarity index 54% rename from packages/techdocs-cli/scripts/build.sh rename to packages/techdocs-cli/scripts/prepack.sh index 31c2eae6c3..6b769de37c 100755 --- a/packages/techdocs-cli/scripts/build.sh +++ b/packages/techdocs-cli/scripts/prepack.sh @@ -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!" diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index 4d71de3e52..a6a671c2df 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -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 /package `. + 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, diff --git a/packages/techdocs-cli/src/lib/httpServer.ts b/packages/techdocs-cli/src/lib/httpServer.ts index 0402ce4e69..92fc5abc87 100644 --- a/packages/techdocs-cli/src/lib/httpServer.ts +++ b/packages/techdocs-cli/src/lib/httpServer.ts @@ -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' }], diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 88b75d99b0..aaacd05815 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -214,6 +214,17 @@ export class CatalogIdentityClient { resolveCatalogMembership(query: MemberClaimQuery): Promise; } +// @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 ``` diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index d1057b19a8..c1130b4270 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.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', diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 2e2d26db61..82a43c7c64 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -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, }); } diff --git a/plugins/auth-backend/src/lib/oauth/helpers.test.ts b/plugins/auth-backend/src/lib/oauth/helpers.test.ts index a98b684141..2f79cdef98 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.test.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.test.ts @@ -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, }); }); diff --git a/plugins/auth-backend/src/lib/oauth/helpers.ts b/plugins/auth-backend/src/lib/oauth/helpers.ts index 878083e679..eec25696a3 100644 --- a/plugins/auth-backend/src/lib/oauth/helpers.ts +++ b/plugins/auth-backend/src/lib/oauth/helpers.ts @@ -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 }; }; diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 7779b67e04..1207254e11 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -43,6 +43,7 @@ export type { AuthHandlerResult, SignInResolver, SignInInfo, + CookieConfigurer, } from './types'; // These types are needed for a postMessage from the login pop-up diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index b02e8d74b8..f3a1a95919 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -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 = { diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index bdb68929b1..ec1a52ffc9 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -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, diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index c00930d2ce..4f5d247a06 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -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; diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index 4d37e24632..c401bc2553 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -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. * diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index b2845ac78d..58e60ab76a 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -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, }); diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts index 351a4983d9..e0e63d86fc 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -102,6 +102,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { client, provider.tenantId, { + userExpand: provider.userExpand, userFilter: provider.userFilter, userGroupMemberFilter: provider.userGroupMemberFilter, groupFilter: provider.groupFilter, diff --git a/plugins/newrelic-dashboard/api-report.md b/plugins/newrelic-dashboard/api-report.md index ffdd5a2833..cff24d90c6 100644 --- a/plugins/newrelic-dashboard/api-report.md +++ b/plugins/newrelic-dashboard/api-report.md @@ -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) diff --git a/plugins/newrelic-dashboard/src/index.ts b/plugins/newrelic-dashboard/src/index.ts index 821ccd4bda..060306524f 100644 --- a/plugins/newrelic-dashboard/src/index.ts +++ b/plugins/newrelic-dashboard/src/index.ts @@ -17,5 +17,6 @@ export { newRelicDashboardPlugin, EntityNewRelicDashboardCard, EntityNewRelicDashboardContent, + DashboardSnapshotComponent, } from './plugin'; export { isNewRelicDashboardAvailable } from './Router'; diff --git a/plugins/newrelic-dashboard/src/plugin.ts b/plugins/newrelic-dashboard/src/plugin.ts index 18397902f7..9bfecff18f 100644 --- a/plugins/newrelic-dashboard/src/plugin.ts +++ b/plugins/newrelic-dashboard/src/plugin.ts @@ -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), + }, + }), +); diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index d6933cd26b..4c28caad1b 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -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) => { + {!currentStepId && } +
diff --git a/scripts/create-release-tag.js b/scripts/create-release-tag.js index 042b00866a..637467f285 100755 --- a/scripts/create-release-tag.js +++ b/scripts/create-release-tag.js @@ -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 => { diff --git a/storybook/package.json b/storybook/package.json index 3864e04484..ea488e94ac 100644 --- a/storybook/package.json +++ b/storybook/package.json @@ -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" } } diff --git a/yarn.lock b/yarn.lock index 386ba1955c..3f7b855e74 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"