diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 35d354fefc..fbb99eec31 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,4 +4,5 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @spotify/backstage-core +* @spotify/backstage-core +/plugins/techdocs @spotify/techdocs-core diff --git a/.gitignore b/.gitignore index f5794babbc..a1eb87e8d5 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,6 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test + +# Temporary change files created by Vim +*.swp diff --git a/packages/app/package.json b/packages/app/package.json index 6aa82a0b36..6b872156d6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -14,7 +14,9 @@ "@backstage/plugin-scaffolder": "^0.1.1-alpha.9", "@backstage/plugin-sentry": "^0.1.1-alpha.9", "@backstage/plugin-tech-radar": "^0.1.1-alpha.9", + "@backstage/plugin-techdocs": "^0.1.1-alpha.9", "@backstage/plugin-welcome": "^0.1.1-alpha.9", + "@backstage/test-utils": "^0.1.1-alpha.9", "@backstage/theme": "^0.1.1-alpha.9", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", @@ -22,6 +24,7 @@ "react": "^16.12.0", "react-dom": "^16.12.0", "react-hot-loader": "^4.12.21", + "react-router": "6.0.0-alpha.5", "react-router-dom": "6.0.0-alpha.5", "react-use": "^14.2.0", "zen-observable": "^0.8.15" diff --git a/packages/app/src/App.test.tsx b/packages/app/src/App.test.tsx index 79f78e45e0..65e8ff64e6 100644 --- a/packages/app/src/App.test.tsx +++ b/packages/app/src/App.test.tsx @@ -15,23 +15,24 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { renderWithEffects } from '@backstage/test-utils'; import App from './App'; describe('App', () => { - beforeAll(() => { - Object.defineProperty(window, 'matchMedia', { - value: jest.fn(() => { - return { - matches: true, - addListener: jest.fn(), - removeListener: jest.fn(), - }; - }), + it('should render', async () => { + Object.defineProperty(process.env, 'APP_CONFIG', { + configurable: true, + value: [ + { + data: { + app: { title: 'Test' }, + }, + context: 'test', + }, + ], }); - }); - it('should render', () => { - const rendered = render(); + + const rendered = await renderWithEffects(); expect(rendered.baseElement).toBeInTheDocument(); }); }); diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index a6485835f7..05373d7d96 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -23,3 +23,4 @@ export { plugin as Circleci } from '@backstage/plugin-circleci'; export { plugin as RegisterComponent } from '@backstage/plugin-register-component'; export { plugin as Sentry } from '@backstage/plugin-sentry'; export { plugin as GitopsProfiles } from '@backstage/plugin-gitops-profiles'; +export { plugin as TechDocs } from '@backstage/plugin-techdocs'; diff --git a/packages/backend-common/src/hot.ts b/packages/backend-common/src/hot.ts index 3d4252b378..c725096c75 100644 --- a/packages/backend-common/src/hot.ts +++ b/packages/backend-common/src/hot.ts @@ -14,10 +14,38 @@ * limitations under the License. */ +// Find all active hot module APIs of all ancestors of a module, including the module itself +function findAllAncestors(_module: NodeModule): NodeModule[] { + const ancestors = new Array(); + const parentIds = new Set(); + + function add(id: string | number, m: NodeModule) { + if (parentIds.has(id)) { + return; + } + parentIds.add(id); + ancestors.push(m); + + for (const parentId of (m as any).parents) { + const parent = require.cache[parentId]; + if (parent) { + add(parentId, parent); + } + } + } + + add(_module.id, _module); + + return ancestors; +} + /** - * This function allows devs to cleanup - * ongoing effects when module gets hot-reloaded + * useHotCleanup allows cleanup of ongoing effects when a module is + * hot-reloaded during development. The cleanup function will be called + * whenever the module itself or any of its parent modules is hot-reloaded. + * * Useful for cleaning intervals, timers, requests etc + * * @example * ```ts * const intervalId = setInterval(doStuff, 1000); @@ -28,69 +56,69 @@ */ export function useHotCleanup(_module: NodeModule, cancelEffect: () => void) { if (_module.hot) { - _module.hot.addDisposeHandler(() => { - cancelEffect(); - }); + const ancestors = findAllAncestors(_module); + let cancelled = false; + + const handler = () => { + if (!cancelled) { + cancelled = true; + cancelEffect(); + } + }; + + for (const m of ancestors) { + m.hot?.addDisposeHandler(handler); + } } } +const CURRENT_HOT_MEMOIZE_INDEX_KEY = 'backstage.io/hmr-memoize-key'; + /** - * This function allows devs to preserve - * some value between hot-reloads. - * Useful for stateful parts of the backend + * Memoizes a generated value across hot-module reloads. This is useful for + * stateful parts of the backend, e.g. to retain a database. + * * @example * ```ts * const db = useHotMemoize(module, () => createDB(dbParams)); * ``` - * @param _module Reference to the current module where you invoke the fn - * @param valueFactory Fn that returns the value you want to memoize + * * @warning Don't use inside conditionals or loops, * same rules as for hooks apply (https://reactjs.org/docs/hooks-rules.html) + * + * @param _module Reference to the current module where you invoke the fn + * @param valueFactory Fn that returns the value you want to memoize */ export function useHotMemoize( _module: NodeModule, valueFactory: () => T, ): T { - const CURRENT_HOT_MEMOIZE_INDEX_KEY = 'backstage.io/hmr-memoize-key'; - if (!_module.hot) { - // Just return value straight away return valueFactory(); } - if (_module.hot && typeof _module.hot.data === 'undefined') { - // First run, init the module data + // When starting blank, reset the counter + if (!_module.hot.data?.[CURRENT_HOT_MEMOIZE_INDEX_KEY]) { + for (const ancestor of findAllAncestors(_module)) { + ancestor.hot?.addDisposeHandler(data => { + data[CURRENT_HOT_MEMOIZE_INDEX_KEY] = 1; + }); + } + _module.hot.data = { - [CURRENT_HOT_MEMOIZE_INDEX_KEY]: 0, + ..._module.hot.data, + [CURRENT_HOT_MEMOIZE_INDEX_KEY]: 1, }; } - // Let's store data per module based on the order of the code invocation - const index = _module.hot.data[CURRENT_HOT_MEMOIZE_INDEX_KEY]; - // Increasing the counter after each call - _module.hot.data[CURRENT_HOT_MEMOIZE_INDEX_KEY] += 1; + // Store data per module, based on the order of the code invocation + const index = _module.hot.data[CURRENT_HOT_MEMOIZE_INDEX_KEY]++; + const value = _module.hot.data[index] ?? valueFactory(); - const prevValue = _module.hot.data[index]; - const createDisposeHandler = (value: any) => (data: { - [key: number]: any; - [indexKey: string]: number; - }) => { - // Preserving the value through the HMR process + // Always add a handler that, upon a HMR event, reinstates the value. + _module.hot.addDisposeHandler(data => { data[index] = value; - // Decreasing the counter after each handler - data[CURRENT_HOT_MEMOIZE_INDEX_KEY] = - // First hot update is still different, need to populate the data - typeof data[CURRENT_HOT_MEMOIZE_INDEX_KEY] === 'undefined' - ? _module.hot!.data[CURRENT_HOT_MEMOIZE_INDEX_KEY] - 1 - : data[CURRENT_HOT_MEMOIZE_INDEX_KEY] - 1; - }; + }); - if (prevValue) { - _module.hot!.addDisposeHandler(createDisposeHandler(prevValue)); - return prevValue; - } - - const newValue = valueFactory(); - _module.hot.addDisposeHandler(createDisposeHandler(newValue)); - return newValue; + return value; } diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index a9c687cbc4..b24dd6ca6b 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -19,7 +19,8 @@ import { PluginEnvironment } from '../types'; export default async function createPlugin({ logger, + database, config, }: PluginEnvironment) { - return await createRouter({ logger, config }); + return await createRouter({ logger, config, database }); } diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 6fe8092e50..aab244f7e9 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -178,8 +178,7 @@ export function createBackendConfig( context: paths.targetPath, entry: [ 'webpack/hot/poll?100', - paths.targetEntry, - ...(paths.targetRunFile ? [paths.targetRunFile] : []), + paths.targetRunFile ? paths.targetRunFile : paths.targetEntry, ], resolve: { extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'], diff --git a/packages/cli/templates/default-app/package.json.hbs b/packages/cli/templates/default-app/package.json.hbs index a96061a5bd..c70e00e4d0 100644 --- a/packages/cli/templates/default-app/package.json.hbs +++ b/packages/cli/templates/default-app/package.json.hbs @@ -31,6 +31,9 @@ "lerna": "^3.20.2", "prettier": "^1.19.1" }, + "resolutions": { + "**/esbuild": "0.5.3" + }, "prettier": "@spotify/prettier-config", "lint-staged": { "*.{js,jsx,ts,tsx}": [ diff --git a/packages/cli/templates/default-app/packages/app/package.json.hbs b/packages/cli/templates/default-app/packages/app/package.json.hbs index 346e09a76a..79120e0d7e 100644 --- a/packages/cli/templates/default-app/packages/app/package.json.hbs +++ b/packages/cli/templates/default-app/packages/app/package.json.hbs @@ -8,10 +8,12 @@ "@material-ui/lab": "4.0.0-alpha.45", "@backstage/cli": "^{{version}}", "@backstage/core": "^{{version}}", + "@backstage/test-utils": "^{{version}}", "@backstage/theme": "^{{version}}", "plugin-welcome": "0.0.0", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-router": "6.0.0-alpha.5", "react-router-dom": "6.0.0-alpha.5", "react-use": "^14.2.0" }, diff --git a/packages/cli/templates/default-app/packages/app/public/index.html b/packages/cli/templates/default-app/packages/app/public/index.html index 3d01107696..ea9208ca57 100644 --- a/packages/cli/templates/default-app/packages/app/public/index.html +++ b/packages/cli/templates/default-app/packages/app/public/index.html @@ -8,47 +8,38 @@ name="description" content="Backstage is an open platform for building developer portals" /> - + - - - + + - Backstage + <%= app.title %> - +