Merge remote-tracking branch 'upstream/master'
This commit is contained in:
@@ -41,6 +41,7 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
- run: yarn tsc
|
||||
- run: yarn build
|
||||
- name: verify app and plugin creation on Windows
|
||||
working-directory: ${{ runner.temp }}
|
||||
|
||||
@@ -56,14 +56,20 @@ jobs:
|
||||
- name: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: verify plugin template
|
||||
run: yarn lerna -- run diff -- --check
|
||||
|
||||
- name: lint
|
||||
run: yarn lerna -- run lint --since origin/master
|
||||
|
||||
- name: build
|
||||
run: yarn build
|
||||
- name: type checking and declarations
|
||||
run: yarn tsc --incremental false
|
||||
|
||||
- name: build changed packages
|
||||
if: ${{ steps.yarn-lock.outcome == 'success' }}
|
||||
# Need to build all dependencies as well to be able to run tests later
|
||||
run: yarn lerna -- run build --since origin/master --include-dependencies
|
||||
|
||||
- name: build all packages
|
||||
if: ${{ steps.yarn-lock.outcome == 'failure' }}
|
||||
run: yarn lerna -- run build
|
||||
|
||||
- name: test changed packages
|
||||
if: ${{ steps.yarn-lock.outcome == 'success' }}
|
||||
@@ -73,8 +79,11 @@ jobs:
|
||||
if: ${{ steps.yarn-lock.outcome == 'failure' }}
|
||||
run: yarn lerna -- run test -- --coverage
|
||||
|
||||
- name: yarn bundle, if app was changed
|
||||
run: git diff --quiet origin/master HEAD -- yarn.lock packages/app packages/core || yarn bundle
|
||||
- name: verify plugin template
|
||||
run: yarn lerna -- run diff -- --check
|
||||
|
||||
- name: bundle example app
|
||||
run: yarn bundle
|
||||
|
||||
- name: verify storybook
|
||||
run: yarn workspace storybook build-storybook
|
||||
|
||||
@@ -54,6 +54,9 @@ jobs:
|
||||
- name: lint
|
||||
run: yarn lerna -- run lint
|
||||
|
||||
- name: type checking and declarations
|
||||
run: yarn tsc --incremental false
|
||||
|
||||
- name: build
|
||||
run: yarn build
|
||||
|
||||
|
||||
@@ -62,11 +62,12 @@ To run a Backstage app, you will need to have the following installed:
|
||||
- [NodeJS](https://nodejs.org/en/download/) - Active LTS Release, currently v12
|
||||
- [yarn](https://classic.yarnpkg.com/en/docs/install)
|
||||
|
||||
After cloning this repo, open a terminal window and start the web app using the following commands from the project root:
|
||||
After cloning this repo, open a terminal window and start the example app using the following commands from the project root:
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
yarn start
|
||||
yarn install # Install dependencies
|
||||
|
||||
yarn start # Start dev server, use --check to enable linting and type-checks
|
||||
```
|
||||
|
||||
The final `yarn start` command should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal.
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# Custom App Themes
|
||||
|
||||
Backstage ships with a default theme with a light and dark mode variant. The themes are provided as
|
||||
a part of the [@backstage/theme](https://www.npmjs.com/package/@backstage/theme) package, which also includes
|
||||
utilities for customizing the default theme, or creating completely new themes.
|
||||
|
||||
## Creating a Custom Theme
|
||||
|
||||
The easiest way to create a new theme is to use the `createTheme` function exported by the [@backstage/theme](https://www.npmjs.com/package/@backstage/theme) package. You can use it to override so basic parameters of the default theme such as the color palette and font.
|
||||
|
||||
For example, you can create a new theme based on the default light theme like this:
|
||||
|
||||
```ts
|
||||
import { createTheme, lightTheme } from '@backstage/theme';
|
||||
|
||||
const myTheme = createTheme({
|
||||
palette: lightTheme.palette,
|
||||
fontFamily: 'Comic Sans MS',
|
||||
});
|
||||
```
|
||||
|
||||
If you want more control over the theme, and for example customize font sizes and margins, you can use the lower-level `createThemeOverrides` function exported by [@backstage/theme](https://www.npmjs.com/package/@backstage/theme) in combination with [createMuiTheme](https://material-ui.com/customization/theming/#createmuitheme-options-args-theme) from [@material-ui/core](https://www.npmjs.com/package/@material-ui/core). See the [@backstage/theme source](https://github.com/spotify/backstage/tree/master/packages/theme/src) and the implementation of the `createTheme` function for how this is done.
|
||||
|
||||
You can also create a theme from scratch that matches the `BackstageTheme` type exported by [@backstage/theme](https://www.npmjs.com/package/@backstage/theme). See the [material-ui docs on theming](https://material-ui.com/customization/theming/) for more information about how that can be done.
|
||||
|
||||
## Using your Custom Theme
|
||||
|
||||
To add a custom theme to your Backstage app, you pass it as configuration to `createApp`.
|
||||
|
||||
For example, adding the theme that we created in the previous section can be done like this:
|
||||
|
||||
```ts
|
||||
import { createApp } from '@backstage/core';
|
||||
|
||||
const app = createApp({
|
||||
apis: ...,
|
||||
plugins: ...,
|
||||
themes: [{
|
||||
id: 'my-theme',
|
||||
title: 'My Custom Theme',
|
||||
variant: 'light',
|
||||
theme: myTheme,
|
||||
}]
|
||||
})
|
||||
```
|
||||
|
||||
Note that your list of custom themes overrides the default themes. If you still want to use the default themes, they are exported as `lightTheme` and `darkTheme` from [@backstage/theme](https://www.npmjs.com/package/@backstage/theme).
|
||||
@@ -24,6 +24,15 @@ plugin directly by navigating to `http://localhost:3000/my-plugin`._
|
||||
<img src='https://github.com/spotify/backstage/raw/master/docs/getting-started/my-plugin_screenshot.png' width='600' alt='my plugin'>
|
||||
</p>
|
||||
|
||||
You can also serve the plugin in isolation by running `yarn start` in the plugin directory. Or by using the yarn workspace command, for example:
|
||||
|
||||
```bash
|
||||
yarn workspace @backstage/plugin-welcome start # Also supports --check
|
||||
```
|
||||
|
||||
This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads.
|
||||
It is only meant for local development, and the setup for it can be found inside the plugin's `dev/` directory.
|
||||
|
||||
[Next Step - Structure of a plugin](structure-of-a-plugin.md)
|
||||
|
||||
[Back to Getting Started](README.md)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Development Environment
|
||||
|
||||
## Serving the Example App
|
||||
|
||||
Open a terminal window and start the web app using the following commands from the project root:
|
||||
|
||||
```bash
|
||||
@@ -21,6 +23,40 @@ You can now view example-app in the browser.
|
||||
On Your Network: http://192.168.1.224:8080
|
||||
```
|
||||
|
||||
## Editor
|
||||
|
||||
The Backstage development environment does not require any specific editor, but it is intended to be used with one that has built-in linting and type-checking. The development server does not include any checks by default, but they can be enabled using the `--check` flag. Note that using the flag may consume more system resources and slow things down.
|
||||
|
||||
## Package Scripts
|
||||
|
||||
There are many commands to be found in the root [package.json](package.json), here are some useful ones:
|
||||
|
||||
```python
|
||||
yarn start # Start serving the example app, use --check to include type checks and linting
|
||||
|
||||
yarn storybook # Start local storybook, useful for working on components in @backstage/core
|
||||
|
||||
yarn workspace @backstage/plugin-welcome start # Serve welcome plugin only, also supports --check
|
||||
|
||||
yarn tsc # Run typecheck, use --watch for watch mode
|
||||
|
||||
yarn build # Build published versions of packages, depends on tsc
|
||||
|
||||
yarn lint # lint packages that have changed since later commit on origin/master
|
||||
yarn lint:all # lint all packages
|
||||
|
||||
yarn test # test packages that have changed since later commit on origin/master
|
||||
yarn test:all # test all packages
|
||||
|
||||
yarn clean # Remove all output folders and @backstage/cli cache
|
||||
|
||||
yarn bundle # Build a production bundle of the example app
|
||||
|
||||
yarn diff # Make sure all plugins are up to date with the latest plugin template
|
||||
|
||||
yarn create-plugin # Create a new plugin
|
||||
```
|
||||
|
||||
### (Optional)Try on Docker
|
||||
|
||||
Run the following commands if you have Docker environment
|
||||
|
||||
+7
-4
@@ -6,12 +6,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn workspace example-app start",
|
||||
"bundle": "yarn build && yarn workspace example-app bundle",
|
||||
"bundle": "yarn workspace example-app bundle",
|
||||
"build": "lerna run build",
|
||||
"clean": "lerna run clean",
|
||||
"tsc": "tsc",
|
||||
"clean": "backstage-cli clean && lerna run clean",
|
||||
"diff": "lerna run diff --",
|
||||
"test": "yarn build && lerna run test --since origin/master -- --coverage",
|
||||
"test:all": "yarn build && lerna run test -- --coverage",
|
||||
"test": "lerna run test --since origin/master -- --coverage",
|
||||
"test:all": "lerna run test -- --coverage",
|
||||
"lint": "lerna run lint --since origin/master --",
|
||||
"lint:all": "lerna run lint --",
|
||||
"docker-build": "yarn bundle && docker build . -t spotify/backstage",
|
||||
@@ -19,6 +20,7 @@
|
||||
"remove-plugin": "backstage-cli remove-plugin",
|
||||
"release": "if [ \"$(git symbolic-ref --short HEAD)\" = master ]; then echo \"don't try to release master\"; exit 1; else lerna version --no-push; fi",
|
||||
"lerna": "lerna",
|
||||
"postinstall": "patch-package",
|
||||
"storybook": "yarn workspace storybook start"
|
||||
},
|
||||
"workspaces": {
|
||||
@@ -34,6 +36,7 @@
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"lerna": "^3.20.2",
|
||||
"lint-staged": "^10.1.0",
|
||||
"patch-package": "^6.2.2",
|
||||
"prettier": "^2.0.5"
|
||||
},
|
||||
"husky": {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"@testing-library/cypress": "^6.0.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/jquery": "^3.3.34",
|
||||
"@types/node": "^12.0.0",
|
||||
|
||||
@@ -19,27 +19,30 @@ import PropTypes from 'prop-types';
|
||||
import { Link, makeStyles, Typography } from '@material-ui/core';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import ExploreIcon from '@material-ui/icons/Explore';
|
||||
import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import AccountTreeIcon from '@material-ui/icons/AccountTree';
|
||||
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarPage,
|
||||
sidebarConfig,
|
||||
SidebarContext,
|
||||
SidebarItem,
|
||||
SidebarSpacer,
|
||||
SidebarDivider,
|
||||
SidebarSearchField,
|
||||
SidebarSpace,
|
||||
SidebarUserBadge,
|
||||
SidebarThemeToggle,
|
||||
} from '@backstage/core';
|
||||
|
||||
const useSidebarLogoStyles = makeStyles({
|
||||
root: {
|
||||
height: sidebarConfig.drawerWidthClosed,
|
||||
width: sidebarConfig.drawerWidthClosed,
|
||||
height: 3 * sidebarConfig.logoHeight,
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
alignItems: 'center',
|
||||
marginBottom: -14,
|
||||
},
|
||||
logoContainer: {
|
||||
width: sidebarConfig.drawerWidthClosed,
|
||||
@@ -48,9 +51,9 @@ const useSidebarLogoStyles = makeStyles({
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontSize: sidebarConfig.logoHeight,
|
||||
fontWeight: 'bold',
|
||||
marginLeft: 22,
|
||||
marginLeft: 20,
|
||||
whiteSpace: 'nowrap',
|
||||
color: '#fff',
|
||||
},
|
||||
@@ -61,7 +64,7 @@ const useSidebarLogoStyles = makeStyles({
|
||||
|
||||
const SidebarLogo: FC<{}> = () => {
|
||||
const classes = useSidebarLogoStyles();
|
||||
const isOpen = useContext(SidebarContext);
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
@@ -75,21 +78,30 @@ const SidebarLogo: FC<{}> = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const handleSearch = (query: string): void => {
|
||||
// XXX (@koroeskohr): for testing purposes
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(query);
|
||||
};
|
||||
|
||||
const Root: FC<{}> = ({ children }) => (
|
||||
<SidebarPage>
|
||||
<Sidebar>
|
||||
<SidebarLogo />
|
||||
<SidebarSpacer />
|
||||
<SidebarSearchField onSearch={handleSearch} />
|
||||
<SidebarDivider />
|
||||
{/* Global nav, not org-specific */}
|
||||
<SidebarItem icon={HomeIcon} to="/" text="Home" />
|
||||
<SidebarItem icon={ExploreIcon} to="/explore" text="Explore" />
|
||||
<SidebarItem icon={CreateComponentIcon} to="/create" text="Create..." />
|
||||
{/* End global nav */}
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={AccountTreeIcon} to="/catalog" text="Catalog" />
|
||||
<SidebarItem icon={AccountCircle} to="/login" text="Login" />
|
||||
<SidebarDivider />
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarThemeToggle />
|
||||
<SidebarUserBadge />
|
||||
</Sidebar>
|
||||
{children}
|
||||
</SidebarPage>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"extends": "@spotify/web-scripts/config/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react",
|
||||
"incremental": false,
|
||||
"types": ["node", "jest"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
+45
-22
@@ -14,45 +14,68 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
|
||||
// If the package has it's own jest config, we use that instead. It will have to
|
||||
// manually extend @spotify/web-scripts/config/jest.config.js that is desired
|
||||
if (fs.existsSync('jest.config.js')) {
|
||||
module.exports = require(path.resolve('jest.config.js'));
|
||||
} else if (fs.existsSync('jest.config.ts')) {
|
||||
module.exports = require(path.resolve('jest.config.ts'));
|
||||
} else {
|
||||
const extraOptions = {
|
||||
modulePaths: ['<rootDir>'],
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
|
||||
'.+\\.(png|jpg|ttf|woff|woff2)$': 'identity-obj-proxy',
|
||||
},
|
||||
async function getConfig() {
|
||||
// If the package has it's own jest config, we use that instead.
|
||||
if (await fs.pathExists('jest.config.js')) {
|
||||
return require(path.resolve('jest.config.js'));
|
||||
} else if (await fs.pathExists('jest.config.ts')) {
|
||||
return require(path.resolve('jest.config.ts'));
|
||||
}
|
||||
|
||||
const moduleNameMapper = {
|
||||
'\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
|
||||
};
|
||||
|
||||
// Only point to src/ if we're not in CI, there we just build packages first anyway
|
||||
if (!process.env.CI) {
|
||||
const LernaProject = require('@lerna/project');
|
||||
const project = new LernaProject(path.resolve('.'));
|
||||
const packages = await project.getPackages();
|
||||
|
||||
// To avoid having to build all deps inside the monorepo before running tests,
|
||||
// we point directory to src/ where applicable.
|
||||
// For example, @backstage/core = <repo-root>/packages/core/src/index.ts is added to moduleNameMapper
|
||||
for (const pkg of packages) {
|
||||
const mainSrc = pkg.get('main:src');
|
||||
if (mainSrc) {
|
||||
moduleNameMapper[pkg.name] = path.resolve(pkg.location, mainSrc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const options = {
|
||||
rootDir: path.resolve('src'),
|
||||
coverageDirectory: path.resolve('coverage'),
|
||||
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'],
|
||||
moduleNameMapper,
|
||||
|
||||
// We build .esm.js files with plugin:build, so to be able to load these in tests they need to be transformed
|
||||
// TODO: jest is working on module support, it's possible that we can remove this in the future
|
||||
transform: {
|
||||
'\\.esm\\.js$': require.resolve('jest-esm-transformer'),
|
||||
'\\.(js|jsx|ts|tsx)': require.resolve('ts-jest'),
|
||||
},
|
||||
// Default behaviour is to not apply transforms for node_modules, but we still want to tranform .esm.js files
|
||||
|
||||
// Default behaviour is to not apply transforms for node_modules, but we still want
|
||||
// to apply the esm-transformer to .esm.js files, since that's what we use in backstage packages.
|
||||
transformIgnorePatterns: ['/node_modules/(?!.*\\.esm\\.js$)'],
|
||||
};
|
||||
|
||||
// Use src/setupTests.ts as the default location for configuring test env
|
||||
if (fs.existsSync('src/setupTests.ts')) {
|
||||
extraOptions.setupFilesAfterEnv = ['<rootDir>/setupTests.ts'];
|
||||
options.setupFilesAfterEnv = ['<rootDir>/setupTests.ts'];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
// We base the jest config on web-scripts, it's pretty flat so we skip any complex merging of config objects
|
||||
// Config can be found here: https://github.com/spotify/web-scripts/blob/master/packages/web-scripts/config/jest.config.js
|
||||
...require('@spotify/web-scripts/config/jest.config.js'),
|
||||
|
||||
...extraOptions,
|
||||
return {
|
||||
...options,
|
||||
|
||||
// If the package has a jest object in package.json we merge that config in. This is the recommended
|
||||
// location for configuring tests.
|
||||
...require(path.resolve('package.json')).jest,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getConfig();
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"noEmit": false,
|
||||
"emitDeclarationOnly": true,
|
||||
"incremental": true,
|
||||
"target": "ES2019",
|
||||
"module": "ESNext",
|
||||
"removeComments": false,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ScriptHost", "ES2019"],
|
||||
|
||||
@@ -29,15 +29,19 @@
|
||||
"backstage-cli": "bin/backstage-cli"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hot-loader/react-dom": "^16.13.0",
|
||||
"@lerna/package-graph": "^3.18.5",
|
||||
"@lerna/project": "^3.18.0",
|
||||
"@rollup/plugin-commonjs": "^11.0.2",
|
||||
"@rollup/plugin-json": "^4.0.2",
|
||||
"@rollup/plugin-node-resolve": "^7.1.1",
|
||||
"@spotify/web-scripts": "^6.0.0",
|
||||
"@sucrase/webpack-loader": "^2.0.0",
|
||||
"bfj": "^7.0.2",
|
||||
"chalk": "^4.0.0",
|
||||
"chokidar": "^3.3.1",
|
||||
"commander": "^4.1.1",
|
||||
"css-loader": "^3.5.3",
|
||||
"dashify": "^2.0.0",
|
||||
"diff": "^4.0.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
@@ -50,27 +54,37 @@
|
||||
"jest": "^25.1.0",
|
||||
"jest-css-modules": "^2.1.0",
|
||||
"jest-esm-transformer": "^1.0.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"ora": "^4.0.3",
|
||||
"raw-loader": "^4.0.1",
|
||||
"react": "^16.0.0",
|
||||
"react-dev-utils": "^10.2.0",
|
||||
"react-scripts": "^3.4.1",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"recursive-readdir": "^2.2.2",
|
||||
"replace-in-file": "^6.0.0",
|
||||
"rollup": "^2.3.2",
|
||||
"rollup-plugin-dts": "^1.4.6",
|
||||
"rollup-plugin-esbuild": "^1.4.1",
|
||||
"rollup-plugin-image-files": "^1.4.2",
|
||||
"rollup-plugin-peer-deps-external": "^2.2.2",
|
||||
"rollup-plugin-postcss": "^3.1.1",
|
||||
"rollup-plugin-typescript2": "^0.26.0",
|
||||
"style-loader": "^1.2.1",
|
||||
"sucrase": "^3.14.1",
|
||||
"tar": "^6.0.1",
|
||||
"ts-loader": "^7.0.4",
|
||||
"url-loader": "^4.1.0",
|
||||
"webpack": "^4.41.6",
|
||||
"webpack-dev-server": "^3.10.3"
|
||||
"webpack-dev-server": "^3.10.3",
|
||||
"yml-loader": "^2.1.0",
|
||||
"yn": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/diff": "^4.0.2",
|
||||
"@types/fs-extra": "^8.1.0",
|
||||
"@types/html-webpack-plugin": "^3.2.2",
|
||||
"@types/inquirer": "^6.5.0",
|
||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||
"@types/node": "^13.7.2",
|
||||
"@types/ora": "^3.2.0",
|
||||
"@types/react-dev-utils": "^9.0.4",
|
||||
|
||||
@@ -14,15 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { run } from '../../lib/run';
|
||||
import { buildBundle } from '../../lib/bundler';
|
||||
import { Command } from 'commander';
|
||||
|
||||
export default async () => {
|
||||
const args = ['build'];
|
||||
|
||||
await run('react-scripts', args, {
|
||||
env: {
|
||||
EXTEND_ESLINT: 'true',
|
||||
SKIP_PREFLIGHT_CHECK: 'true',
|
||||
},
|
||||
export default async (cmd: Command) => {
|
||||
await buildBundle({
|
||||
entry: 'src/index',
|
||||
statsJsonEnabled: cmd.stats,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -14,20 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { run } from '../../lib/run';
|
||||
import { createLogFunc } from '../../lib/logging';
|
||||
import { watchDeps } from '../../lib/watchDeps';
|
||||
import { serveBundle } from '../../lib/bundler';
|
||||
import { Command } from 'commander';
|
||||
|
||||
export default async () => {
|
||||
// Start dynamic watch and build of dependencies, then serve the app
|
||||
await watchDeps({ build: true });
|
||||
|
||||
await run('react-scripts', ['start'], {
|
||||
env: {
|
||||
EXTEND_ESLINT: 'true',
|
||||
SKIP_PREFLIGHT_CHECK: 'true',
|
||||
},
|
||||
// We need to avoid clearing the terminal, or the build feedback of dependencies will be lost
|
||||
stdoutLogFunc: createLogFunc(process.stdout),
|
||||
export default async (cmd: Command) => {
|
||||
const waitForExit = await serveBundle({
|
||||
entry: 'src/index',
|
||||
checksEnabled: cmd.check,
|
||||
});
|
||||
|
||||
await waitForExit();
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ import inquirer, { Answers, Question } from 'inquirer';
|
||||
import { exec as execCb } from 'child_process';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import os from 'os';
|
||||
import { Task, templatingTask } from '../../lib/tasks';
|
||||
import { Task, templatingTask, installWithLocalDeps } from '../../lib/tasks';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { version } from '../../lib/version';
|
||||
const exec = promisify(execCb);
|
||||
@@ -57,19 +57,22 @@ async function cleanUp(tempDir: string) {
|
||||
});
|
||||
}
|
||||
|
||||
async function buildApp(appFolder: string) {
|
||||
const commands = ['yarn install', 'yarn build'];
|
||||
for (const command of commands) {
|
||||
await Task.forItem('executing', command, async () => {
|
||||
process.chdir(appFolder);
|
||||
async function buildApp(appDir: string) {
|
||||
const runCmd = async (cmd: string) => {
|
||||
await Task.forItem('executing', cmd, async () => {
|
||||
process.chdir(appDir);
|
||||
|
||||
await exec(command).catch((error) => {
|
||||
await exec(cmd).catch((error) => {
|
||||
process.stdout.write(error.stderr);
|
||||
process.stdout.write(error.stdout);
|
||||
throw new Error(`Could not execute command ${chalk.cyan(command)}`);
|
||||
throw new Error(`Could not execute command ${chalk.cyan(cmd)}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
await installWithLocalDeps(appDir);
|
||||
await runCmd('yarn tsc');
|
||||
await runCmd('yarn build');
|
||||
}
|
||||
|
||||
export async function moveApp(
|
||||
@@ -86,44 +89,6 @@ export async function moveApp(
|
||||
});
|
||||
}
|
||||
|
||||
async function addPackageResolutions(appDir: string) {
|
||||
const pkgJsonPath = resolvePath(appDir, 'package.json');
|
||||
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||
|
||||
pkgJson.resolutions = pkgJson.resolutions || {};
|
||||
pkgJson.dependencies = pkgJson.dependencies || {};
|
||||
|
||||
const depNames = [
|
||||
'cli',
|
||||
'core',
|
||||
'dev-utils',
|
||||
'test-utils',
|
||||
'test-utils-core',
|
||||
'theme',
|
||||
];
|
||||
|
||||
for (const name of depNames) {
|
||||
await Task.forItem(
|
||||
'adding',
|
||||
`@backstage/${name} link to package.json`,
|
||||
async () => {
|
||||
const pkgPath = paths.resolveOwnRoot('packages', name);
|
||||
// Add to both resolutions and dependencies, or transitive dependencies will still be fetched from the registry.
|
||||
pkgJson.dependencies[`@backstage/${name}`] = `file:${pkgPath}`;
|
||||
pkgJson.resolutions[`@backstage/${name}`] = `file:${pkgPath}`;
|
||||
|
||||
await fs
|
||||
.writeJSON(pkgJsonPath, pkgJson, { encoding: 'utf8', spaces: 2 })
|
||||
.catch((error) => {
|
||||
throw new Error(
|
||||
`Failed to add resolutions to package.json: ${error.message}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
const questions: Question[] = [
|
||||
{
|
||||
@@ -164,12 +129,6 @@ export default async () => {
|
||||
Task.section('Moving to final location');
|
||||
await moveApp(tempDir, appDir, answers.name);
|
||||
|
||||
// e2e testing needs special treatment
|
||||
if (process.env.BACKSTAGE_E2E_CLI_TEST) {
|
||||
Task.section('Linking packages locally for e2e tests');
|
||||
await addPackageResolutions(appDir);
|
||||
}
|
||||
|
||||
Task.section('Building the app');
|
||||
await buildApp(appDir);
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from '../../lib/codeowners';
|
||||
import { paths } from '../../lib/paths';
|
||||
import { version } from '../../lib/version';
|
||||
import { Task, templatingTask } from '../../lib/tasks';
|
||||
import { Task, templatingTask, installWithLocalDeps } from '../../lib/tasks';
|
||||
const exec = promisify(execCb);
|
||||
|
||||
async function checkExists(rootDir: string, id: string) {
|
||||
@@ -141,7 +141,9 @@ async function cleanUp(tempDir: string) {
|
||||
}
|
||||
|
||||
async function buildPlugin(pluginFolder: string) {
|
||||
const commands = ['yarn install', 'yarn build'];
|
||||
await installWithLocalDeps(paths.targetRoot);
|
||||
|
||||
const commands = ['yarn tsc', 'yarn build'];
|
||||
for (const command of commands) {
|
||||
await Task.forItem('executing', command, async () => {
|
||||
process.chdir(pluginFolder);
|
||||
|
||||
@@ -14,85 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { rollup, watch, OutputOptions } from 'rollup';
|
||||
import { Command } from 'commander';
|
||||
import chalk from 'chalk';
|
||||
import { withCache, getDefaultCacheOptions } from '../../lib/buildCache';
|
||||
import { paths } from '../../lib/paths';
|
||||
import conf from './rollup.config';
|
||||
import { buildPackage } from '../../lib/packager';
|
||||
|
||||
function logError(error: any) {
|
||||
console.log('');
|
||||
|
||||
if (error.code === 'PLUGIN_ERROR') {
|
||||
// typescript2 plugin has a complete message with all codeframes
|
||||
if (error.plugin === 'rpt2') {
|
||||
console.log(error.message);
|
||||
} else {
|
||||
// Log which plugin is causing errors to make it easier to identity.
|
||||
// If we see these in logs we likely want to provide some custom error
|
||||
// output for those plugins too.
|
||||
console.log(`(plugin ${error.plugin}) ${error}`);
|
||||
}
|
||||
} else {
|
||||
// Generic rollup errors, log what's available
|
||||
if (error.loc) {
|
||||
const file = `${paths.resolveTarget((error.loc.file || error.id)!)}`;
|
||||
const pos = `${error.loc.line}:${error.loc.column}`;
|
||||
console.log(`${file} [${pos}]`);
|
||||
} else if (error.id) {
|
||||
console.log(paths.resolveTarget(error.id));
|
||||
}
|
||||
|
||||
console.log(String(error));
|
||||
|
||||
if (error.url) {
|
||||
console.log(chalk.cyan(error.url));
|
||||
}
|
||||
|
||||
if (error.frame) {
|
||||
console.log(chalk.dim(error.frame));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default async (cmd: Command) => {
|
||||
if (cmd.watch) {
|
||||
// We're not resolving this promise because watch() doesn't have any exit event.
|
||||
// Instead we just wait until the user sends an interrupt signal.
|
||||
await new Promise(() => {
|
||||
const watcher = watch(conf);
|
||||
watcher.on('event', (event) => {
|
||||
// START — the watcher is (re)starting
|
||||
// BUNDLE_START — building an individual bundle
|
||||
// BUNDLE_END — finished building a bundle
|
||||
// END — finished building all bundles
|
||||
// ERROR — encountered an error while bundling
|
||||
|
||||
if (event.code === 'ERROR') {
|
||||
logError(event.error);
|
||||
} else if (event.code === 'BUNDLE_START') {
|
||||
console.log(chalk.dim(`building ${event.input}`));
|
||||
} else if (event.code === 'BUNDLE_END') {
|
||||
const { input, duration } = event;
|
||||
const s = (duration / 1000).toFixed(1);
|
||||
console.log(chalk.green(`built ${input} in ${s}s`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
await withCache(getDefaultCacheOptions(), async () => {
|
||||
try {
|
||||
const bundle = await rollup({
|
||||
input: conf.input,
|
||||
plugins: conf.plugins,
|
||||
});
|
||||
await bundle.generate(conf.output as OutputOptions);
|
||||
await bundle.write(conf.output as OutputOptions);
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
export default async () => {
|
||||
await buildPackage();
|
||||
};
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 peerDepsExternal from 'rollup-plugin-peer-deps-external';
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import postcss from 'rollup-plugin-postcss';
|
||||
import imageFiles from 'rollup-plugin-image-files';
|
||||
import json from '@rollup/plugin-json';
|
||||
import { RollupWatchOptions } from 'rollup';
|
||||
import { paths } from '../../lib/paths';
|
||||
|
||||
export default {
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/index.esm.js',
|
||||
format: 'module',
|
||||
},
|
||||
plugins: [
|
||||
peerDepsExternal({
|
||||
includeDependencies: true,
|
||||
}),
|
||||
resolve({
|
||||
mainFields: ['browser', 'module', 'main'],
|
||||
}),
|
||||
commonjs({
|
||||
include: ['node_modules/**', '../../node_modules/**'],
|
||||
exclude: ['**/*.stories.*', '**/*.test.*'],
|
||||
}),
|
||||
postcss(),
|
||||
imageFiles(),
|
||||
json(),
|
||||
typescript({
|
||||
include: `${paths.resolveTarget('src')}/**/*.{js,jsx,ts,tsx}`,
|
||||
tsconfigOverride: {
|
||||
// The dev folder is for the local plugin serve, ignore it in the build
|
||||
// If we don't do this we get a folder structure similar to dist/{src,dev}/...
|
||||
exclude: ['dev'],
|
||||
compilerOptions: {
|
||||
// Use absolute path to src dir as root for declarations, relying on the default
|
||||
// seems to produce declaration maps that are relative to dist/ instead of src/
|
||||
// Using a relative path like ../src doesn't work either becaus it will be used as is in subdirs.
|
||||
sourceRoot: paths.resolveTarget('src'),
|
||||
},
|
||||
},
|
||||
clean: true,
|
||||
}),
|
||||
],
|
||||
} as RollupWatchOptions;
|
||||
+8
-8
@@ -14,14 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { startDevServer } from './server';
|
||||
import { watchDeps } from '../../../lib/watchDeps';
|
||||
import { serveBundle } from '../../lib/bundler';
|
||||
import { Command } from 'commander';
|
||||
|
||||
export default async () => {
|
||||
await watchDeps({ build: true });
|
||||
export default async (cmd: Command) => {
|
||||
const waitForExit = await serveBundle({
|
||||
entry: 'dev/index',
|
||||
checksEnabled: cmd.check,
|
||||
});
|
||||
|
||||
await startDevServer();
|
||||
|
||||
// Wait for interrupt signal
|
||||
await new Promise(() => {});
|
||||
await waitForExit();
|
||||
};
|
||||
@@ -30,11 +30,13 @@ const main = (argv: string[]) => {
|
||||
program
|
||||
.command('app:build')
|
||||
.description('Build an app for a production release')
|
||||
.option('--stats', 'Write bundle stats to output directory')
|
||||
.action(actionHandler(() => require('./commands/app/build')));
|
||||
|
||||
program
|
||||
.command('app:serve')
|
||||
.description('Serve an app for local development')
|
||||
.option('--check', 'Enable type checking and linting')
|
||||
.action(actionHandler(() => require('./commands/app/serve')));
|
||||
|
||||
program
|
||||
@@ -53,13 +55,13 @@ const main = (argv: string[]) => {
|
||||
|
||||
program
|
||||
.command('plugin:build')
|
||||
.option('--watch', 'Enable watch mode')
|
||||
.description('Build a plugin')
|
||||
.action(actionHandler(() => require('./commands/plugin/build')));
|
||||
|
||||
program
|
||||
.command('plugin:serve')
|
||||
.description('Serves the dev/ folder of a plugin')
|
||||
.option('--check', 'Enable type checking and linting')
|
||||
.action(actionHandler(() => require('./commands/plugin/serve')));
|
||||
|
||||
program
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 yn from 'yn';
|
||||
import fs from 'fs-extra';
|
||||
import { resolve as resolvePath } from 'path';
|
||||
import webpack from 'webpack';
|
||||
import {
|
||||
measureFileSizesBeforeBuild,
|
||||
printFileSizesAfterBuild,
|
||||
} from 'react-dev-utils/FileSizeReporter';
|
||||
import formatWebpackMessages from 'react-dev-utils/formatWebpackMessages';
|
||||
import { createConfig } from './config';
|
||||
import { BuildOptions } from './types';
|
||||
import { resolveBundlingPaths } from './paths';
|
||||
import chalk from 'chalk';
|
||||
|
||||
// TODO(Rugvip): Limits from CRA, we might want to tweak these though.
|
||||
const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024;
|
||||
const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024;
|
||||
|
||||
export async function buildBundle(options: BuildOptions) {
|
||||
const { statsJsonEnabled } = options;
|
||||
|
||||
const paths = resolveBundlingPaths(options);
|
||||
const config = createConfig(paths, {
|
||||
...options,
|
||||
checksEnabled: false,
|
||||
isDev: false,
|
||||
});
|
||||
const compiler = webpack(config);
|
||||
|
||||
const isCi = yn(process.env.CI, { default: false });
|
||||
|
||||
const previousFileSizes = await measureFileSizesBeforeBuild(paths.targetDist);
|
||||
await fs.emptyDir(paths.targetDist);
|
||||
|
||||
const { stats } = await build(compiler, isCi).catch((error) => {
|
||||
console.log(chalk.red('Failed to compile.\n'));
|
||||
throw new Error(`Failed to compile.\n${error.message || error}`);
|
||||
});
|
||||
|
||||
if (statsJsonEnabled) {
|
||||
// No @types/bfj
|
||||
await require('bfj').write(
|
||||
resolvePath(paths.targetDist, 'bundle-stats.json'),
|
||||
stats.toJson(),
|
||||
);
|
||||
}
|
||||
|
||||
printFileSizesAfterBuild(
|
||||
stats,
|
||||
previousFileSizes,
|
||||
paths.targetDist,
|
||||
WARN_AFTER_BUNDLE_GZIP_SIZE,
|
||||
WARN_AFTER_CHUNK_GZIP_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
async function build(compiler: webpack.Compiler, isCi: boolean) {
|
||||
const stats = await new Promise<webpack.Stats>((resolve, reject) => {
|
||||
compiler.run((err, buildStats) => {
|
||||
if (err) {
|
||||
if (err.message) {
|
||||
const { errors } = formatWebpackMessages({
|
||||
errors: [err.message],
|
||||
warnings: new Array<string>(),
|
||||
} as webpack.Stats.ToJsonOutput);
|
||||
|
||||
throw new Error(errors[0]);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
} else {
|
||||
resolve(buildStats);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const { errors, warnings } = formatWebpackMessages(
|
||||
stats.toJson({ all: false, warnings: true, errors: true }),
|
||||
);
|
||||
|
||||
if (errors.length) {
|
||||
// Only keep the first error. Others are often indicative
|
||||
// of the same problem, but confuse the reader with noise.
|
||||
throw new Error(errors[0]);
|
||||
}
|
||||
if (isCi && warnings.length) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'\nTreating warnings as errors because process.env.CI = true.\n',
|
||||
),
|
||||
);
|
||||
throw new Error(warnings.join('\n\n'));
|
||||
}
|
||||
|
||||
return { stats };
|
||||
}
|
||||
+53
-79
@@ -15,91 +15,28 @@
|
||||
*/
|
||||
|
||||
import webpack from 'webpack';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
|
||||
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
|
||||
import { Paths } from './paths';
|
||||
import { BundlingPaths } from './paths';
|
||||
import { transforms } from './transforms';
|
||||
import { optimization } from './optimization';
|
||||
import { BundlingOptions } from './types';
|
||||
// import checkRequiredFiles from 'react-dev-utils/checkRequiredFiles';
|
||||
// import ModuleNotFoundPlugin from 'react-dev-utils/ModuleNotFoundPlugin';
|
||||
// import errorOverlayMiddleware from 'react-dev-utils/errorOverlayMiddleware';
|
||||
// import evalSourceMapMiddleware from 'react-dev-utils/evalSourceMapMiddleware';
|
||||
// import WatchMissingNodeModulesPlugin from 'react-dev-utils/WatchMissingNodeModulesPlugin';
|
||||
|
||||
export function createConfig(paths: Paths): webpack.Configuration {
|
||||
return {
|
||||
mode: 'development',
|
||||
profile: false,
|
||||
bail: false,
|
||||
devtool: 'cheap-module-eval-source-map',
|
||||
context: paths.targetPath,
|
||||
entry: [
|
||||
`${require.resolve('webpack-dev-server/client')}?/`,
|
||||
require.resolve('webpack/hot/dev-server'),
|
||||
paths.targetDevEntry,
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
|
||||
plugins: [
|
||||
new ModuleScopePlugin(
|
||||
[paths.targetSrc, paths.targetDev],
|
||||
[paths.targetPackageJson],
|
||||
),
|
||||
],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(tsx?|jsx?|mjs)$/,
|
||||
enforce: 'pre',
|
||||
include: [paths.targetSrc, paths.targetDev],
|
||||
use: {
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
emitWarning: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(tsx?|jsx?|mjs)$/,
|
||||
include: [paths.targetSrc, paths.targetDev],
|
||||
exclude: /node_modules/,
|
||||
loader: 'ts-loader',
|
||||
options: {
|
||||
// disable type checker - handled by ForkTsCheckerWebpackPlugin
|
||||
transpileOnly: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.frag/, /\.xml/],
|
||||
loader: 'url-loader',
|
||||
include: paths.targetAssets,
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.ya?ml$/,
|
||||
use: 'yml-loader',
|
||||
},
|
||||
{
|
||||
include: /\.(md)$/,
|
||||
use: 'raw-loader',
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
output: {
|
||||
publicPath: '/',
|
||||
filename: 'bundle.js',
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: paths.targetHtml,
|
||||
}),
|
||||
export function createConfig(
|
||||
paths: BundlingPaths,
|
||||
options: BundlingOptions,
|
||||
): webpack.Configuration {
|
||||
const { checksEnabled, isDev } = options;
|
||||
|
||||
const { plugins, loaders } = transforms(paths, options);
|
||||
|
||||
if (checksEnabled) {
|
||||
plugins.push(
|
||||
new ForkTsCheckerWebpackPlugin({
|
||||
tsconfig: paths.targetTsConfig,
|
||||
eslint: true,
|
||||
@@ -111,8 +48,45 @@ export function createConfig(paths: Paths): webpack.Configuration {
|
||||
},
|
||||
reportFiles: ['**', '!**/__tests__/**', '!**/?(*.)(spec|test).*'],
|
||||
}),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
mode: isDev ? 'development' : 'production',
|
||||
profile: false,
|
||||
bail: false,
|
||||
performance: {
|
||||
hints: false, // we check the gzip size instead
|
||||
},
|
||||
devtool: isDev ? 'cheap-module-eval-source-map' : 'source-map',
|
||||
context: paths.targetPath,
|
||||
entry: [require.resolve('react-hot-loader/patch'), paths.targetEntry],
|
||||
resolve: {
|
||||
extensions: ['.ts', '.tsx', '.mjs', '.js', '.jsx'],
|
||||
mainFields: ['main:src', 'browser', 'module', 'main'],
|
||||
plugins: [
|
||||
new ModuleScopePlugin(
|
||||
[paths.targetSrc, paths.targetDev],
|
||||
[paths.targetPackageJson],
|
||||
),
|
||||
],
|
||||
alias: {
|
||||
'react-dom': '@hot-loader/react-dom',
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: loaders,
|
||||
},
|
||||
output: {
|
||||
path: paths.targetDist,
|
||||
publicPath: '/',
|
||||
filename: isDev ? '[name].js' : '[name].[hash:8].js',
|
||||
chunkFilename: isDev
|
||||
? '[name].chunk.js'
|
||||
: '[name].[chunkhash:8].chunk.js',
|
||||
},
|
||||
optimization: optimization(options),
|
||||
plugins,
|
||||
node: {
|
||||
module: 'empty',
|
||||
dgram: 'empty',
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { buildBundle } from './bundle';
|
||||
export { serveBundle } from './server';
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Options } from 'webpack';
|
||||
import { BundlingOptions } from './types';
|
||||
|
||||
export const optimization = (
|
||||
options: BundlingOptions,
|
||||
): Options.Optimization => {
|
||||
const { isDev } = options;
|
||||
|
||||
return {
|
||||
minimize: !isDev,
|
||||
runtimeChunk: 'single',
|
||||
splitChunks: {
|
||||
automaticNameDelimiter: '-',
|
||||
cacheGroups: {
|
||||
default: false,
|
||||
// Put all vendor code needed for initial page load in individual files if they're big
|
||||
// enough, if they're smaller they end up in the main
|
||||
packages: {
|
||||
chunks: 'initial',
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name(module: any) {
|
||||
// get the name. E.g. node_modules/packageName/not/this/part.js
|
||||
// or node_modules/packageName
|
||||
const packageName = module.context.match(
|
||||
/[\\/]node_modules[\\/](.*?)([\\/]|$)/,
|
||||
)[1];
|
||||
|
||||
// npm package names are URL-safe, but some servers don't like @ symbols
|
||||
return packageName.replace('@', '');
|
||||
},
|
||||
filename: isDev
|
||||
? 'module-[name].js'
|
||||
: 'module-[name].[chunkhash:8].js',
|
||||
priority: 10,
|
||||
minSize: 100000,
|
||||
minChunks: 1,
|
||||
maxAsyncRequests: Infinity,
|
||||
maxInitialRequests: Infinity,
|
||||
} as any, // filename is not included in type, but we need it
|
||||
// Group together the smallest modules
|
||||
vendor: {
|
||||
chunks: 'initial',
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: 'vendor',
|
||||
priority: 5,
|
||||
enforce: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
+14
-6
@@ -15,9 +15,16 @@
|
||||
*/
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import { paths } from '../../../lib/paths';
|
||||
import { paths } from '../paths';
|
||||
|
||||
export type BundlingPathsOptions = {
|
||||
// bundle entrypoint, e.g. 'src/index'
|
||||
entry: string;
|
||||
};
|
||||
|
||||
export function resolveBundlingPaths(options: BundlingPathsOptions) {
|
||||
const { entry } = options;
|
||||
|
||||
export function getPaths() {
|
||||
const resolveTargetModule = (path: string) => {
|
||||
for (const ext of ['mjs', 'js', 'ts', 'tsx', 'jsx']) {
|
||||
const filePath = paths.resolveTarget(`${path}.${ext}`);
|
||||
@@ -28,7 +35,7 @@ export function getPaths() {
|
||||
return paths.resolveTarget(`${path}.js`);
|
||||
};
|
||||
|
||||
let targetHtml = paths.resolveTarget('dev/index.html');
|
||||
let targetHtml = paths.resolveTarget(`${entry}.html`);
|
||||
if (!existsSync(targetHtml)) {
|
||||
targetHtml = paths.resolveOwn('templates/serve_index.html');
|
||||
}
|
||||
@@ -36,14 +43,15 @@ export function getPaths() {
|
||||
return {
|
||||
targetHtml,
|
||||
targetPath: paths.resolveTarget('.'),
|
||||
targetDist: paths.resolveTarget('dist'),
|
||||
targetAssets: paths.resolveTarget('assets'),
|
||||
targetSrc: paths.resolveTarget('src'),
|
||||
targetDev: paths.resolveTarget('dev'),
|
||||
targetDevEntry: resolveTargetModule('dev/index'),
|
||||
targetTsConfig: paths.resolveTarget('tsconfig.json'),
|
||||
targetEntry: resolveTargetModule(entry),
|
||||
targetTsConfig: paths.resolveTargetRoot('tsconfig.json'),
|
||||
targetNodeModules: paths.resolveTarget('node_modules'),
|
||||
targetPackageJson: paths.resolveTarget('package.json'),
|
||||
};
|
||||
}
|
||||
|
||||
export type Paths = ReturnType<typeof getPaths>;
|
||||
export type BundlingPaths = ReturnType<typeof resolveBundlingPaths>;
|
||||
+26
-7
@@ -14,33 +14,37 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import yn from 'yn';
|
||||
import webpack from 'webpack';
|
||||
import WebpackDevServer from 'webpack-dev-server';
|
||||
import openBrowser from 'react-dev-utils/openBrowser';
|
||||
import { choosePort, prepareUrls } from 'react-dev-utils/WebpackDevServerUtils';
|
||||
import { getPaths } from './paths';
|
||||
import { createConfig } from './config';
|
||||
import { ServeOptions } from './types';
|
||||
import { resolveBundlingPaths } from './paths';
|
||||
|
||||
export async function startDevServer() {
|
||||
export async function serveBundle(options: ServeOptions) {
|
||||
const host = process.env.HOST ?? '0.0.0.0';
|
||||
const defaultPort = parseInt(process.env.PORT ?? '', 10) || 3000;
|
||||
|
||||
const port = await choosePort(host, defaultPort);
|
||||
if (!port) {
|
||||
return;
|
||||
throw new Error(`Invalid or no port set: '${port}'`);
|
||||
}
|
||||
|
||||
const protocol = process.env.HTTPS === 'true' ? 'https' : 'http';
|
||||
const protocol = yn(process.env.HTTPS, { default: false }) ? 'https' : 'http';
|
||||
const urls = prepareUrls(protocol, host, port);
|
||||
|
||||
const paths = getPaths();
|
||||
const config = createConfig(paths);
|
||||
const paths = resolveBundlingPaths(options);
|
||||
const config = createConfig(paths, { ...options, isDev: true });
|
||||
const compiler = webpack(config);
|
||||
|
||||
const server = new WebpackDevServer(compiler, {
|
||||
hot: true,
|
||||
publicPath: '/',
|
||||
historyApiFallback: true,
|
||||
quiet: true,
|
||||
clientLogLevel: 'warning',
|
||||
stats: 'errors-warnings',
|
||||
https: protocol === 'https',
|
||||
host,
|
||||
port,
|
||||
@@ -57,4 +61,19 @@ export async function startDevServer() {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const waitForExit = async () => {
|
||||
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
|
||||
process.on(signal, () => {
|
||||
server.close();
|
||||
// exit instead of resolve. The process is shutting down and resolving a promise here logs an error
|
||||
process.exit();
|
||||
});
|
||||
}
|
||||
|
||||
// Block indefinitely and wait for the interrupt signal
|
||||
return new Promise(() => {});
|
||||
};
|
||||
|
||||
return waitForExit;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 webpack, { Module, Plugin } from 'webpack';
|
||||
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
|
||||
import { BundlingOptions } from './types';
|
||||
import { BundlingPaths } from './paths';
|
||||
|
||||
type Transforms = {
|
||||
loaders: Module['rules'];
|
||||
plugins: Plugin[];
|
||||
};
|
||||
|
||||
export const transforms = (
|
||||
paths: BundlingPaths,
|
||||
options: BundlingOptions,
|
||||
): Transforms => {
|
||||
const { isDev } = options;
|
||||
|
||||
const loaders = [
|
||||
{
|
||||
test: /\.(tsx?)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: require.resolve('@sucrase/webpack-loader'),
|
||||
options: {
|
||||
transforms: ['typescript', 'jsx', 'react-hot-loader'],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(jsx?|mjs)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: require.resolve('@sucrase/webpack-loader'),
|
||||
options: {
|
||||
transforms: ['jsx', 'react-hot-loader'],
|
||||
},
|
||||
},
|
||||
{
|
||||
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.frag/, /\.xml/],
|
||||
loader: require.resolve('url-loader'),
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: 'static/media/[name].[hash:8].[ext]',
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.ya?ml$/,
|
||||
use: require.resolve('yml-loader'),
|
||||
},
|
||||
{
|
||||
include: /\.(md)$/,
|
||||
use: require.resolve('raw-loader'),
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
use: [
|
||||
isDev ? require.resolve('style-loader') : MiniCssExtractPlugin.loader,
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const plugins = new Array<Plugin>();
|
||||
|
||||
plugins.push(
|
||||
new HtmlWebpackPlugin({
|
||||
template: paths.targetHtml,
|
||||
}),
|
||||
);
|
||||
|
||||
if (isDev) {
|
||||
plugins.push(new webpack.HotModuleReplacementPlugin());
|
||||
} else {
|
||||
plugins.push(
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].[contenthash:8].css',
|
||||
chunkFilename: '[name].[id].[contenthash:8].css',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return { loaders, plugins };
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { BundlingPathsOptions } from './paths';
|
||||
|
||||
export type BundlingOptions = {
|
||||
checksEnabled: boolean;
|
||||
isDev: boolean;
|
||||
};
|
||||
|
||||
export type ServeOptions = BundlingPathsOptions & {
|
||||
checksEnabled: boolean;
|
||||
};
|
||||
|
||||
export type BuildOptions = BundlingPathsOptions & {
|
||||
statsJsonEnabled: boolean;
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { relative as relativePath } from 'path';
|
||||
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import postcss from 'rollup-plugin-postcss';
|
||||
import esbuild from 'rollup-plugin-esbuild';
|
||||
import imageFiles from 'rollup-plugin-image-files';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import json from '@rollup/plugin-json';
|
||||
import { RollupOptions } from 'rollup';
|
||||
|
||||
import { paths } from '../paths';
|
||||
|
||||
export const makeConfigs = async (): Promise<RollupOptions[]> => {
|
||||
const typesInput = paths.resolveTargetRoot(
|
||||
'dist',
|
||||
relativePath(paths.targetRoot, paths.targetDir),
|
||||
'src/index.d.ts',
|
||||
);
|
||||
|
||||
const declarationsExist = await fs.pathExists(typesInput);
|
||||
if (!declarationsExist) {
|
||||
const path = relativePath(paths.targetDir, typesInput);
|
||||
throw new Error(
|
||||
`No declaration files found at ${path}, be sure to run tsc to generate .d.ts files before packaging`,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/index.esm.js',
|
||||
format: 'module',
|
||||
},
|
||||
plugins: [
|
||||
peerDepsExternal({
|
||||
includeDependencies: true,
|
||||
}),
|
||||
resolve({
|
||||
mainFields: ['browser', 'module', 'main'],
|
||||
}),
|
||||
commonjs({
|
||||
include: ['node_modules/**', '../../node_modules/**'],
|
||||
exclude: ['**/*.stories.*', '**/*.test.*'],
|
||||
}),
|
||||
postcss(),
|
||||
imageFiles(),
|
||||
json(),
|
||||
esbuild({
|
||||
target: 'es2019',
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
input: typesInput,
|
||||
output: {
|
||||
file: 'dist/index.d.ts',
|
||||
format: 'es',
|
||||
},
|
||||
plugins: [dts()],
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { buildPackage } from './packager';
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { rollup, RollupOptions } from 'rollup';
|
||||
import chalk from 'chalk';
|
||||
import { relative as relativePath } from 'path';
|
||||
import { paths } from '../paths';
|
||||
import { makeConfigs } from './config';
|
||||
|
||||
function formatErrorMessage(error: any) {
|
||||
let msg = '';
|
||||
|
||||
if (error.code === 'PLUGIN_ERROR') {
|
||||
if (error.plugin === 'esbuild') {
|
||||
msg += `${error.message}\n\n`;
|
||||
for (const { text, location } of error.errors) {
|
||||
const { line, column } = location;
|
||||
const path = relativePath(paths.targetDir, error.id);
|
||||
const loc = chalk.cyan(`${path}:${line}:${column}`);
|
||||
|
||||
if (text === 'Unexpected "<"' && error.id.endsWith('.js')) {
|
||||
msg += `${loc}: ${text}, JavaScript files with JSX should use a .jsx extension`;
|
||||
} else {
|
||||
msg += `${loc}: ${text}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Log which plugin is causing errors to make it easier to identity.
|
||||
// If we see these in logs we likely want to provide some custom error
|
||||
// output for those plugins too.
|
||||
msg += `(plugin ${error.plugin}) ${error}\n`;
|
||||
}
|
||||
} else {
|
||||
// Generic rollup errors, log what's available
|
||||
if (error.loc) {
|
||||
const file = `${paths.resolveTarget((error.loc.file || error.id)!)}`;
|
||||
const pos = `${error.loc.line}:${error.loc.column}`;
|
||||
msg += `${file} [${pos}]\n`;
|
||||
} else if (error.id) {
|
||||
msg += `${paths.resolveTarget(error.id)}\n`;
|
||||
}
|
||||
|
||||
msg += `${error}\n`;
|
||||
|
||||
if (error.url) {
|
||||
msg += `${chalk.cyan(error.url)}\n`;
|
||||
}
|
||||
|
||||
if (error.frame) {
|
||||
msg += `${chalk.dim(error.frame)}\n`;
|
||||
}
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
async function build(config: RollupOptions) {
|
||||
try {
|
||||
const bundle = await rollup(config);
|
||||
if (config.output) {
|
||||
for (const output of [config.output].flat()) {
|
||||
await bundle.generate(output);
|
||||
await bundle.write(output);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(formatErrorMessage(error));
|
||||
}
|
||||
}
|
||||
|
||||
export const buildPackage = async () => {
|
||||
const configs = await makeConfigs();
|
||||
await Promise.all(configs.map(build));
|
||||
};
|
||||
@@ -18,8 +18,12 @@ import chalk from 'chalk';
|
||||
import fs from 'fs-extra';
|
||||
import handlebars from 'handlebars';
|
||||
import ora from 'ora';
|
||||
import { basename, dirname } from 'path';
|
||||
import { resolve as resolvePath, basename, dirname } from 'path';
|
||||
import recursive from 'recursive-readdir';
|
||||
import { promisify } from 'util';
|
||||
import { exec as execCb } from 'child_process';
|
||||
import { paths } from './paths';
|
||||
const exec = promisify(execCb);
|
||||
|
||||
const TASK_NAME_MAX_LENGTH = 14;
|
||||
|
||||
@@ -69,7 +73,7 @@ export async function templatingTask(
|
||||
destinationDir: string,
|
||||
context: any,
|
||||
) {
|
||||
const files = await recursive(templateDir).catch(error => {
|
||||
const files = await recursive(templateDir).catch((error) => {
|
||||
throw new Error(`Failed to read template directory: ${error.message}`);
|
||||
});
|
||||
|
||||
@@ -85,7 +89,7 @@ export async function templatingTask(
|
||||
const compiled = handlebars.compile(template.toString());
|
||||
const contents = compiled({ name: basename(destination), ...context });
|
||||
|
||||
await fs.writeFile(destination, contents).catch(error => {
|
||||
await fs.writeFile(destination, contents).catch((error) => {
|
||||
throw new Error(
|
||||
`Failed to create file: ${destination}: ${error.message}`,
|
||||
);
|
||||
@@ -93,7 +97,7 @@ export async function templatingTask(
|
||||
});
|
||||
} else {
|
||||
await Task.forItem('copying', basename(file), async () => {
|
||||
await fs.copyFile(file, destinationFile).catch(error => {
|
||||
await fs.copyFile(file, destinationFile).catch((error) => {
|
||||
const destination = destinationFile;
|
||||
throw new Error(
|
||||
`Failed to copy file to ${destination} : ${error.message}`,
|
||||
@@ -103,3 +107,99 @@ export async function templatingTask(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// List of local packages that we need to modify as a part of an E2E test
|
||||
const PATCH_PACKAGES = [
|
||||
'cli',
|
||||
'core',
|
||||
'dev-utils',
|
||||
'test-utils',
|
||||
'test-utils-core',
|
||||
'theme',
|
||||
];
|
||||
|
||||
// This runs a `yarn install` task, but with special treatment for e2e tests
|
||||
export async function installWithLocalDeps(dir: string) {
|
||||
// This makes us install any package inside this repo as a local file dependency.
|
||||
// For example, instead of trying to fetch @backstage/core from npm, we point it
|
||||
// to <repo-root>/packages/core. This makes yarn use a simple file copy to install it instead.
|
||||
if (process.env.BACKSTAGE_E2E_CLI_TEST) {
|
||||
Task.section('Linking packages locally for e2e tests');
|
||||
|
||||
const pkgJsonPath = resolvePath(dir, 'package.json');
|
||||
const pkgJson = await fs.readJson(pkgJsonPath);
|
||||
|
||||
pkgJson.resolutions = pkgJson.resolutions || {};
|
||||
pkgJson.dependencies = pkgJson.dependencies || {};
|
||||
|
||||
if (!pkgJson.resolutions[`@backstage/${PATCH_PACKAGES[0]}`]) {
|
||||
for (const name of PATCH_PACKAGES) {
|
||||
await Task.forItem(
|
||||
'adding',
|
||||
`@backstage/${name} link to package.json`,
|
||||
async () => {
|
||||
const pkgPath = paths.resolveOwnRoot('packages', name);
|
||||
// Add to both resolutions and dependencies, or transitive dependencies will still be fetched from the registry.
|
||||
pkgJson.dependencies[`@backstage/${name}`] = `file:${pkgPath}`;
|
||||
pkgJson.resolutions[`@backstage/${name}`] = `file:${pkgPath}`;
|
||||
|
||||
await fs
|
||||
.writeJSON(pkgJsonPath, pkgJson, { encoding: 'utf8', spaces: 2 })
|
||||
.catch((error) => {
|
||||
throw new Error(
|
||||
`Failed to add resolutions to package.json: ${error.message}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Task.forItem('executing', 'yarn install', async () => {
|
||||
await exec('yarn install', { cwd: dir }).catch((error) => {
|
||||
process.stdout.write(error.stderr);
|
||||
process.stdout.write(error.stdout);
|
||||
throw new Error(
|
||||
`Could not execute command ${chalk.cyan('yarn install')}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// This takes care of pointing all the installed packages from this repo to
|
||||
// dist instead of the local src.
|
||||
// For example node_modules/@backstage/core/packages.json is rewritten to point
|
||||
// types to dist/index.d.ts and the main:src field is removed.
|
||||
// Without this we get type checking errors in the e2e test
|
||||
if (process.env.BACKSTAGE_E2E_CLI_TEST) {
|
||||
Task.section('Patchling local dependencies for e2e tests');
|
||||
|
||||
for (const name of PATCH_PACKAGES) {
|
||||
await Task.forItem(
|
||||
'patching',
|
||||
`node_modules/@backstage/${name} package.json`,
|
||||
async () => {
|
||||
const depJsonPath = resolvePath(
|
||||
dir,
|
||||
'node_modules/@backstage',
|
||||
name,
|
||||
'package.json',
|
||||
);
|
||||
const depJson = await fs.readJson(depJsonPath);
|
||||
|
||||
// We want dist to be used for e2e tests
|
||||
delete depJson['main:src'];
|
||||
depJson.types = 'dist/index.d.ts';
|
||||
|
||||
await fs
|
||||
.writeJSON(depJsonPath, depJson, { encoding: 'utf8', spaces: 2 })
|
||||
.catch((error) => {
|
||||
throw new Error(
|
||||
`Failed to add resolutions to package.json: ${error.message}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,18 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "yarn workspace app start",
|
||||
"bundle": "yarn build && yarn workspace app bundle",
|
||||
"bundle": "yarn workspace app bundle",
|
||||
"build": "lerna run build",
|
||||
"test": "yarn build && lerna run test --since origin/master -- --coverage",
|
||||
"test:all": "yarn build && lerna run test -- --coverage",
|
||||
"tsc": "tsc",
|
||||
"clean": "backstage-cli clean && lerna run clean",
|
||||
"diff": "lerna run diff --",
|
||||
"test": "lerna run test --since origin/master -- --coverage",
|
||||
"test:all": "lerna run test -- --coverage",
|
||||
"lint": "lerna run lint --since origin/master --",
|
||||
"lint:all": "lerna run lint --",
|
||||
"create-plugin": "backstage-cli create-plugin",
|
||||
"clean": "lerna run clean"
|
||||
"remove-plugin": "backstage-cli remove-plugin",
|
||||
"postinstall": "patch-package"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
@@ -25,6 +29,7 @@
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "^{{version}}",
|
||||
"lerna": "^3.20.2",
|
||||
"patch-package": "^6.2.2",
|
||||
"prettier": "^1.19.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# patches
|
||||
|
||||
This folder contains patches for dependency type declarations. Typescript doesn't provide a way to override of fix types that are installed as a part of the dependent package itself.
|
||||
|
||||
Do not add any more patches here, these patches were added as a part of getting the entire repo type-checked, and we depended on some packages with bad types.
|
||||
|
||||
As soon as the below issues are fixed, we should remove the patching functionality completely.
|
||||
|
||||
## material-table
|
||||
|
||||
Fixed in master but not released yet, tracked here: https://github.com/mbrn/material-table/pull/1624
|
||||
@@ -0,0 +1,12 @@
|
||||
diff --git a/node_modules/material-table/types/index.d.ts b/node_modules/material-table/types/index.d.ts
|
||||
index 06b700b..5b3c765 100644
|
||||
--- a/node_modules/material-table/types/index.d.ts
|
||||
+++ b/node_modules/material-table/types/index.d.ts
|
||||
@@ -228,7 +228,6 @@ export interface Options {
|
||||
showTitle?: boolean;
|
||||
showTextRowsSelected?: boolean;
|
||||
search?: boolean;
|
||||
- searchText?: string;
|
||||
searchFieldAlignment?: 'left' | 'right';
|
||||
searchFieldStyle?: React.CSSProperties;
|
||||
searchText?: string;
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "plugin-welcome",
|
||||
"version": "0.0.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
{
|
||||
"extends": "@backstage/cli/config/tsconfig.json"
|
||||
"extends": "@backstage/cli/config/tsconfig.json",
|
||||
"include": ["packages/*/src", "plugins/*/src", "plugins/*/dev"],
|
||||
"exclude": ["**/node_modules"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@backstage/plugin-{{id}}",
|
||||
"version": "{{version}}",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
@@ -33,7 +34,7 @@
|
||||
"@backstage/dev-utils": "^{{version}}",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "dev"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"extends": "./config/tsconfig.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["**/*.test.*"],
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"emitDeclarationOnly": false,
|
||||
"removeComments": true,
|
||||
"module": "CommonJS"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
@@ -30,6 +31,13 @@
|
||||
"@backstage/theme": "^0.1.1-alpha.5",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-helmet": "^5.0.15",
|
||||
"@types/react-sparklines": "^1.7.0",
|
||||
"@types/zen-observable": "^0.8.0",
|
||||
"classnames": "^2.2.6",
|
||||
"clsx": "^1.1.0",
|
||||
"lodash": "^4.17.15",
|
||||
@@ -53,14 +61,7 @@
|
||||
"@backstage/test-utils-core": "^0.1.1-alpha.5",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/classnames": "^2.2.9",
|
||||
"@types/google-protobuf": "^3.7.2",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react-helmet": "^5.0.15",
|
||||
"@types/react-sparklines": "^1.7.0",
|
||||
"@types/zen-observable": "^0.8.0"
|
||||
"@testing-library/user-event": "^10.2.4"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*.{js,d.ts}"
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Observable from 'zen-observable';
|
||||
import { AppThemeApi, AppTheme } from '../../definitions';
|
||||
import { BehaviorSubject } from '../lib';
|
||||
import { Observable } from '../../../types';
|
||||
|
||||
const STORAGE_KEY = 'theme';
|
||||
|
||||
@@ -50,34 +51,17 @@ export class AppThemeSelector implements AppThemeApi {
|
||||
return selector;
|
||||
}
|
||||
|
||||
private readonly themes: AppTheme[];
|
||||
|
||||
private activeThemeId: string | undefined;
|
||||
private readonly subject = new BehaviorSubject<string | undefined>(undefined);
|
||||
|
||||
private readonly observable: Observable<string | undefined>;
|
||||
private readonly subscribers = new Set<
|
||||
ZenObservable.SubscriptionObserver<string | undefined>
|
||||
>();
|
||||
|
||||
constructor(themes: AppTheme[]) {
|
||||
this.themes = themes;
|
||||
|
||||
this.observable = new Observable((subscriber) => {
|
||||
subscriber.next(this.activeThemeId);
|
||||
|
||||
this.subscribers.add(subscriber);
|
||||
return () => {
|
||||
this.subscribers.delete(subscriber);
|
||||
};
|
||||
});
|
||||
}
|
||||
constructor(private readonly themes: AppTheme[]) {}
|
||||
|
||||
getInstalledThemes(): AppTheme[] {
|
||||
return this.themes.slice();
|
||||
}
|
||||
|
||||
activeThemeId$(): Observable<string | undefined> {
|
||||
return this.observable;
|
||||
return this.subject;
|
||||
}
|
||||
|
||||
getActiveThemeId(): string | undefined {
|
||||
@@ -86,6 +70,6 @@ export class AppThemeSelector implements AppThemeApi {
|
||||
|
||||
setActiveThemeId(themeId?: string): void {
|
||||
this.activeThemeId = themeId;
|
||||
this.subscribers.forEach((subscriber) => subscriber.next(themeId));
|
||||
this.subject.next(themeId);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-17
@@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import Observable from 'zen-observable';
|
||||
import { OAuthScopes } from '../../definitions';
|
||||
import { BehaviorSubject } from '../lib';
|
||||
import { Observable } from '../../../types';
|
||||
|
||||
type RequestQueueEntry<ResultType> = {
|
||||
scopes: OAuthScopes;
|
||||
@@ -44,16 +45,15 @@ export type OAuthPendingRequestsApi<ResultType> = {
|
||||
export class OAuthPendingRequests<ResultType>
|
||||
implements OAuthPendingRequestsApi<ResultType> {
|
||||
private requests: RequestQueueEntry<ResultType>[] = [];
|
||||
private listeners: ZenObservable.SubscriptionObserver<
|
||||
PendingRequest<ResultType>
|
||||
>[] = [];
|
||||
private subject = new BehaviorSubject<PendingRequest<ResultType>>(
|
||||
this.getCurrentPending(),
|
||||
);
|
||||
|
||||
request(scopes: OAuthScopes): Promise<ResultType> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.requests.push({ scopes, resolve, reject });
|
||||
|
||||
const pending = this.getCurrentPending();
|
||||
this.listeners.forEach((listener) => listener.next(pending));
|
||||
this.subject.next(this.getCurrentPending());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,26 +66,18 @@ export class OAuthPendingRequests<ResultType>
|
||||
return true;
|
||||
});
|
||||
|
||||
const pending = this.getCurrentPending();
|
||||
this.listeners.forEach((listener) => listener.next(pending));
|
||||
this.subject.next(this.getCurrentPending());
|
||||
}
|
||||
|
||||
reject(error: Error) {
|
||||
this.requests.forEach((request) => request.reject(error));
|
||||
this.requests = [];
|
||||
|
||||
const pending = this.getCurrentPending();
|
||||
this.listeners.forEach((listener) => listener.next(pending));
|
||||
this.subject.next(this.getCurrentPending());
|
||||
}
|
||||
|
||||
pending(): Observable<PendingRequest<ResultType>> {
|
||||
return new Observable((subscriber) => {
|
||||
this.listeners.push(subscriber);
|
||||
subscriber.next(this.getCurrentPending());
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter((l) => l !== subscriber);
|
||||
};
|
||||
});
|
||||
return this.subject;
|
||||
}
|
||||
|
||||
private getCurrentPending(): PendingRequest<ResultType> {
|
||||
|
||||
+6
-18
@@ -21,8 +21,9 @@ import {
|
||||
AuthRequester,
|
||||
AuthRequesterOptions,
|
||||
} from '../../definitions';
|
||||
import Observable from 'zen-observable';
|
||||
import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests';
|
||||
import { BehaviorSubject } from '../lib';
|
||||
import { Observable } from '../../../types';
|
||||
|
||||
/**
|
||||
* The OAuthRequestManager is an implementation of the OAuthRequestApi.
|
||||
@@ -32,24 +33,10 @@ import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests';
|
||||
* them all together into a single request for each OAuth provider.
|
||||
*/
|
||||
export class OAuthRequestManager implements OAuthRequestApi {
|
||||
private readonly request$: Observable<PendingAuthRequest[]>;
|
||||
private readonly observers = new Set<
|
||||
ZenObservable.SubscriptionObserver<PendingAuthRequest[]>
|
||||
>();
|
||||
private readonly subject = new BehaviorSubject<PendingAuthRequest[]>([]);
|
||||
private currentRequests: PendingAuthRequest[] = [];
|
||||
private handlerCount = 0;
|
||||
|
||||
constructor() {
|
||||
this.request$ = new Observable<PendingAuthRequest[]>((observer) => {
|
||||
observer.next(this.currentRequests);
|
||||
|
||||
this.observers.add(observer);
|
||||
return () => {
|
||||
this.observers.delete(observer);
|
||||
};
|
||||
}).map((requests) => requests.filter(Boolean)); // Convert from sparse array to array of present items only
|
||||
}
|
||||
|
||||
createAuthRequester<T>(options: AuthRequesterOptions<T>): AuthRequester<T> {
|
||||
const handler = new OAuthPendingRequests<T>();
|
||||
|
||||
@@ -66,7 +53,8 @@ export class OAuthRequestManager implements OAuthRequestApi {
|
||||
newRequests[index] = request;
|
||||
}
|
||||
this.currentRequests = newRequests;
|
||||
this.observers.forEach((observer) => observer.next(newRequests));
|
||||
// Convert from sparse array to array of present items only
|
||||
this.subject.next(newRequests.filter(Boolean));
|
||||
},
|
||||
});
|
||||
|
||||
@@ -100,7 +88,7 @@ export class OAuthRequestManager implements OAuthRequestApi {
|
||||
}
|
||||
|
||||
authRequest$(): Observable<PendingAuthRequest[]> {
|
||||
return this.request$;
|
||||
return this.subject;
|
||||
}
|
||||
|
||||
async showLoginPopup(options: LoginPopupOptions): Promise<any> {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export * from './subjects';
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { PublishSubject, BehaviorSubject } from './subjects';
|
||||
|
||||
function observerSpy() {
|
||||
return {
|
||||
next: jest.fn(),
|
||||
error: jest.fn(),
|
||||
complete: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
describe('PublishSubject', () => {
|
||||
it('should be completed', async () => {
|
||||
const subj = new PublishSubject<number>();
|
||||
subj.complete();
|
||||
|
||||
const spy = observerSpy();
|
||||
subj.subscribe(spy);
|
||||
await 'a tick';
|
||||
|
||||
expect(spy.next).not.toHaveBeenCalled();
|
||||
expect(spy.error).not.toHaveBeenCalled();
|
||||
expect(spy.complete).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(() => subj.next(1)).toThrow('PublishSubject is closed');
|
||||
expect(() => subj.error(new Error())).toThrow('PublishSubject is closed');
|
||||
expect(() => subj.complete()).toThrow('PublishSubject is closed');
|
||||
|
||||
expect(subj.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('should forward values to all subscribers', async () => {
|
||||
const subj = new PublishSubject<number>();
|
||||
|
||||
const spy1 = observerSpy();
|
||||
const spy2 = observerSpy();
|
||||
subj.subscribe(spy1);
|
||||
const sub = subj.subscribe(spy2);
|
||||
|
||||
subj.next(42);
|
||||
|
||||
expect(spy1.next).toHaveBeenCalledTimes(1);
|
||||
expect(spy1.next).toHaveBeenCalledWith(42);
|
||||
expect(spy2.next).toHaveBeenCalledTimes(1);
|
||||
expect(spy2.next).toHaveBeenCalledWith(42);
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
subj.next(1337);
|
||||
|
||||
expect(spy1.next).toHaveBeenCalledTimes(2);
|
||||
expect(spy1.next).toHaveBeenCalledWith(1337);
|
||||
expect(spy2.next).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(spy1.error).not.toHaveBeenCalled();
|
||||
expect(spy2.error).not.toHaveBeenCalled();
|
||||
expect(spy1.complete).not.toHaveBeenCalled();
|
||||
expect(spy2.complete).not.toHaveBeenCalled();
|
||||
|
||||
expect(subj.closed).toBe(false);
|
||||
});
|
||||
|
||||
it('should forward errors', async () => {
|
||||
const subj = new PublishSubject<number>();
|
||||
|
||||
const spy1 = observerSpy();
|
||||
subj.subscribe(spy1);
|
||||
|
||||
const error = new Error('NOPE');
|
||||
subj.error(error);
|
||||
expect(spy1.error).toHaveBeenCalledWith(error);
|
||||
expect(spy1.next).not.toHaveBeenCalled();
|
||||
expect(spy1.complete).not.toHaveBeenCalled();
|
||||
|
||||
const spy2 = observerSpy();
|
||||
subj.subscribe(spy2);
|
||||
await 'a tick';
|
||||
expect(spy2.error).toHaveBeenCalledWith(error);
|
||||
expect(spy2.next).not.toHaveBeenCalled();
|
||||
expect(spy2.complete).not.toHaveBeenCalled();
|
||||
|
||||
expect(subj.closed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BehaviorSubject', () => {
|
||||
it('should be completed', async () => {
|
||||
const subj = new BehaviorSubject(0);
|
||||
subj.complete();
|
||||
|
||||
const next = jest.fn();
|
||||
const complete = jest.fn();
|
||||
subj.subscribe({ next, complete });
|
||||
await 'a tick';
|
||||
|
||||
expect(complete).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(() => subj.next(1)).toThrow('BehaviorSubject is closed');
|
||||
expect(() => subj.error(new Error())).toThrow('BehaviorSubject is closed');
|
||||
expect(() => subj.complete()).toThrow('BehaviorSubject is closed');
|
||||
|
||||
expect(subj.closed).toBe(true);
|
||||
});
|
||||
|
||||
it('should forward values to all subscribers', async () => {
|
||||
const subj = new BehaviorSubject(0);
|
||||
|
||||
const obs1 = jest.fn();
|
||||
const obs2 = jest.fn();
|
||||
subj.subscribe(obs1);
|
||||
const sub = subj.subscribe(obs2);
|
||||
await 'a tick';
|
||||
|
||||
expect(obs1).toHaveBeenCalledTimes(1);
|
||||
expect(obs1).toHaveBeenCalledWith(0);
|
||||
expect(obs2).toHaveBeenCalledTimes(1);
|
||||
expect(obs2).toHaveBeenCalledWith(0);
|
||||
|
||||
subj.next(42);
|
||||
|
||||
expect(obs1).toHaveBeenCalledTimes(2);
|
||||
expect(obs1).toHaveBeenCalledWith(42);
|
||||
expect(obs2).toHaveBeenCalledTimes(2);
|
||||
expect(obs2).toHaveBeenCalledWith(42);
|
||||
|
||||
sub.unsubscribe();
|
||||
|
||||
subj.next(1337);
|
||||
|
||||
expect(obs1).toHaveBeenCalledTimes(3);
|
||||
expect(obs1).toHaveBeenCalledWith(1337);
|
||||
expect(obs2).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(subj.closed).toBe(false);
|
||||
});
|
||||
|
||||
it('should forward errors', async () => {
|
||||
const subj = new BehaviorSubject<number>(0);
|
||||
|
||||
const spy1 = observerSpy();
|
||||
subj.subscribe(spy1);
|
||||
await 'a tick';
|
||||
|
||||
expect(spy1.error).not.toHaveBeenCalled();
|
||||
expect(spy1.next).toHaveBeenCalledWith(0);
|
||||
|
||||
const error = new Error('NOPE');
|
||||
subj.error(error);
|
||||
expect(spy1.error).toHaveBeenCalledWith(error);
|
||||
expect(spy1.next).toHaveBeenCalledWith(0);
|
||||
expect(spy1.next).toHaveBeenCalledTimes(1);
|
||||
expect(spy1.complete).not.toHaveBeenCalled();
|
||||
|
||||
const spy2 = observerSpy();
|
||||
subj.subscribe(spy2);
|
||||
await 'a tick';
|
||||
expect(spy2.error).toHaveBeenCalledWith(error);
|
||||
expect(spy2.next).not.toHaveBeenCalled();
|
||||
expect(spy2.complete).not.toHaveBeenCalled();
|
||||
|
||||
expect(subj.closed).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 { Observable } from '../../../types';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
|
||||
// TODO(Rugvip): These are stopgap and probably incomplete implementations of subjects.
|
||||
// If we add a more complete Observables library they should be replaced.
|
||||
|
||||
/**
|
||||
* A basic implementation of ReactiveX publish subjects.
|
||||
*
|
||||
* A subject is a convenient way to create an observable when you want
|
||||
* to fan out a single value to all subscribers.
|
||||
*
|
||||
* See http://reactivex.io/documentation/subject.html
|
||||
*/
|
||||
export class PublishSubject<T>
|
||||
implements Observable<T>, ZenObservable.SubscriptionObserver<T> {
|
||||
private isClosed = false;
|
||||
private terminatingError?: Error;
|
||||
|
||||
private readonly observable = new ObservableImpl<T>((subscriber) => {
|
||||
if (this.isClosed) {
|
||||
if (this.terminatingError) {
|
||||
subscriber.error(this.terminatingError);
|
||||
} else {
|
||||
subscriber.complete();
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
|
||||
this.subscribers.add(subscriber);
|
||||
return () => {
|
||||
this.subscribers.delete(subscriber);
|
||||
};
|
||||
});
|
||||
|
||||
private readonly subscribers = new Set<
|
||||
ZenObservable.SubscriptionObserver<T>
|
||||
>();
|
||||
|
||||
get closed() {
|
||||
return this.isClosed;
|
||||
}
|
||||
|
||||
next(value: T) {
|
||||
if (this.isClosed) {
|
||||
throw new Error('PublishSubject is closed');
|
||||
}
|
||||
this.subscribers.forEach((subscriber) => subscriber.next(value));
|
||||
}
|
||||
|
||||
error(error: Error) {
|
||||
if (this.isClosed) {
|
||||
throw new Error('PublishSubject is closed');
|
||||
}
|
||||
this.isClosed = true;
|
||||
this.terminatingError = error;
|
||||
this.subscribers.forEach((subscriber) => subscriber.error(error));
|
||||
}
|
||||
|
||||
complete() {
|
||||
if (this.isClosed) {
|
||||
throw new Error('PublishSubject is closed');
|
||||
}
|
||||
this.isClosed = true;
|
||||
this.subscribers.forEach((subscriber) => subscriber.complete());
|
||||
}
|
||||
|
||||
subscribe(observer: ZenObservable.Observer<T>): ZenObservable.Subscription;
|
||||
subscribe(
|
||||
onNext: (value: T) => void,
|
||||
onError?: (error: any) => void,
|
||||
onComplete?: () => void,
|
||||
): ZenObservable.Subscription;
|
||||
subscribe(
|
||||
onNext: ZenObservable.Observer<T> | ((value: T) => void),
|
||||
onError?: (error: any) => void,
|
||||
onComplete?: () => void,
|
||||
): ZenObservable.Subscription {
|
||||
const observer =
|
||||
typeof onNext === 'function'
|
||||
? {
|
||||
next: onNext,
|
||||
error: onError,
|
||||
complete: onComplete,
|
||||
}
|
||||
: onNext;
|
||||
|
||||
return this.observable.subscribe(observer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A basic implementation of ReactiveX behavior subjects.
|
||||
*
|
||||
* A subject is a convenient way to create an observable when you want
|
||||
* to fan out a single value to all subscribers.
|
||||
*
|
||||
* The BehaviorSubject will emit the most recently emitted value or error
|
||||
* whenever a new observer subscribes to the subject.
|
||||
*
|
||||
* See http://reactivex.io/documentation/subject.html
|
||||
*/
|
||||
export class BehaviorSubject<T>
|
||||
implements Observable<T>, ZenObservable.SubscriptionObserver<T> {
|
||||
private isClosed = false;
|
||||
private currentValue: T;
|
||||
private terminatingError?: Error;
|
||||
|
||||
constructor(value: T) {
|
||||
this.currentValue = value;
|
||||
}
|
||||
|
||||
private readonly observable = new ObservableImpl<T>((subscriber) => {
|
||||
if (this.isClosed) {
|
||||
if (this.terminatingError) {
|
||||
subscriber.error(this.terminatingError);
|
||||
} else {
|
||||
subscriber.complete();
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
|
||||
subscriber.next(this.currentValue);
|
||||
|
||||
this.subscribers.add(subscriber);
|
||||
return () => {
|
||||
this.subscribers.delete(subscriber);
|
||||
};
|
||||
});
|
||||
|
||||
private readonly subscribers = new Set<
|
||||
ZenObservable.SubscriptionObserver<T>
|
||||
>();
|
||||
|
||||
get closed() {
|
||||
return this.isClosed;
|
||||
}
|
||||
|
||||
next(value: T) {
|
||||
if (this.isClosed) {
|
||||
throw new Error('BehaviorSubject is closed');
|
||||
}
|
||||
this.currentValue = value;
|
||||
this.subscribers.forEach((subscriber) => subscriber.next(value));
|
||||
}
|
||||
|
||||
error(error: Error) {
|
||||
if (this.isClosed) {
|
||||
throw new Error('BehaviorSubject is closed');
|
||||
}
|
||||
this.isClosed = true;
|
||||
this.terminatingError = error;
|
||||
this.subscribers.forEach((subscriber) => subscriber.error(error));
|
||||
}
|
||||
|
||||
complete() {
|
||||
if (this.isClosed) {
|
||||
throw new Error('BehaviorSubject is closed');
|
||||
}
|
||||
this.isClosed = true;
|
||||
this.subscribers.forEach((subscriber) => subscriber.complete());
|
||||
}
|
||||
|
||||
subscribe(observer: ZenObservable.Observer<T>): ZenObservable.Subscription;
|
||||
subscribe(
|
||||
onNext: (value: T) => void,
|
||||
onError?: (error: any) => void,
|
||||
onComplete?: () => void,
|
||||
): ZenObservable.Subscription;
|
||||
subscribe(
|
||||
onNext: ZenObservable.Observer<T> | ((value: T) => void),
|
||||
onError?: (error: any) => void,
|
||||
onComplete?: () => void,
|
||||
): ZenObservable.Subscription {
|
||||
const observer =
|
||||
typeof onNext === 'function'
|
||||
? {
|
||||
next: onNext,
|
||||
error: onError,
|
||||
complete: onComplete,
|
||||
}
|
||||
: onNext;
|
||||
|
||||
return this.observable.subscribe(observer);
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,5 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export type {
|
||||
NavTarget,
|
||||
NavTargetConfig,
|
||||
NavTargetOverrideConfig,
|
||||
} from './types';
|
||||
export * from './types';
|
||||
export { createNavTarget } from './NavTarget';
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ describe('<HorizontalScrollGrid />', () => {
|
||||
jest.spyOn(window.performance, 'now').mockReturnValue(5);
|
||||
jest
|
||||
.spyOn(window, 'requestAnimationFrame')
|
||||
.mockImplementation(cb => cb(20));
|
||||
.mockImplementation((cb) => cb(20));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
+5
-5
@@ -36,7 +36,7 @@ describe('<StructuredMetadataTable />', () => {
|
||||
<StructuredMetadataTable metadata={metadata} />,
|
||||
);
|
||||
const keys = Object.keys(metadata);
|
||||
keys.forEach(value => {
|
||||
keys.forEach((value) => {
|
||||
expect(getByText(startCase(value))).toBeInTheDocument();
|
||||
expect(getByText(metadata[value])).toBeInTheDocument();
|
||||
});
|
||||
@@ -49,7 +49,7 @@ describe('<StructuredMetadataTable />', () => {
|
||||
);
|
||||
|
||||
const keys = Object.keys(metadata);
|
||||
keys.forEach(value => {
|
||||
keys.forEach((value) => {
|
||||
expect(getByText(startCase(value))).toBeInTheDocument();
|
||||
expect(getByText(metadata[value].toString())).toBeInTheDocument();
|
||||
});
|
||||
@@ -61,10 +61,10 @@ describe('<StructuredMetadataTable />', () => {
|
||||
<StructuredMetadataTable metadata={metadata} />,
|
||||
);
|
||||
const keys = Object.keys(metadata);
|
||||
keys.forEach(value => {
|
||||
keys.forEach((value) => {
|
||||
expect(getByText(startCase(value))).toBeInTheDocument();
|
||||
});
|
||||
metadata.arrayField.forEach(value => {
|
||||
metadata.arrayField.forEach((value) => {
|
||||
expect(getByText(value)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -85,7 +85,7 @@ describe('<StructuredMetadataTable />', () => {
|
||||
);
|
||||
|
||||
const keys = Object.keys(metadata.config);
|
||||
keys.forEach(value => {
|
||||
keys.forEach((value) => {
|
||||
expect(
|
||||
getByText(startCase(value), { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
@@ -20,7 +20,7 @@ import React, { FC, useRef, useState } from 'react';
|
||||
import { sidebarConfig, SidebarContext } from './config';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(theme => ({
|
||||
const useStyles = makeStyles<BackstageTheme>((theme) => ({
|
||||
root: {
|
||||
zIndex: 1000,
|
||||
position: 'relative',
|
||||
@@ -115,7 +115,7 @@ export const Sidebar: FC<Props> = ({
|
||||
onBlur={handleClose}
|
||||
data-testid="sidebar-root"
|
||||
>
|
||||
<SidebarContext.Provider value={state === State.Open}>
|
||||
<SidebarContext.Provider value={{ isOpen: state === State.Open }}>
|
||||
<div
|
||||
className={clsx(classes.drawer, {
|
||||
[classes.drawerPeek]: state === State.Peek,
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 React, { FC, useContext, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { Link, Typography, makeStyles, Collapse } from '@material-ui/core';
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import { BackstageTheme } from '@backstage/theme';
|
||||
import {
|
||||
SIDEBAR_INTRO_LOCAL_STORAGE,
|
||||
SidebarContext,
|
||||
sidebarConfig,
|
||||
} from './config';
|
||||
import { SidebarDivider } from './Items';
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>((theme) => ({
|
||||
introCard: {
|
||||
color: '#b5b5b5',
|
||||
// XXX (@koroeskohr): should I be using a Mui theme variable?
|
||||
fontSize: 12,
|
||||
width: sidebarConfig.drawerWidthOpen,
|
||||
marginTop: 18,
|
||||
marginBottom: 12,
|
||||
paddingLeft: sidebarConfig.iconPadding,
|
||||
paddingRight: sidebarConfig.iconPadding,
|
||||
},
|
||||
introDismiss: {
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
marginTop: 12,
|
||||
},
|
||||
introDismissLink: {
|
||||
color: '#dddddd',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginBottom: 4,
|
||||
'&:hover': {
|
||||
color: theme.palette.linkHover,
|
||||
transition: theme.transitions.create('color', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.shortest,
|
||||
}),
|
||||
},
|
||||
},
|
||||
introDismissText: {
|
||||
fontSize: '0.7rem',
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
introDismissIcon: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
marginRight: 12,
|
||||
},
|
||||
}));
|
||||
|
||||
type IntroCardProps = {
|
||||
text: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const IntroCard: FC<IntroCardProps> = (props) => {
|
||||
const classes = useStyles();
|
||||
const { text, onClose } = props;
|
||||
const handleClose = () => onClose();
|
||||
|
||||
return (
|
||||
<div className={classes.introCard}>
|
||||
<Typography variant="subtitle2">{text}</Typography>
|
||||
<div className={classes.introDismiss}>
|
||||
<Link
|
||||
component="button"
|
||||
onClick={handleClose}
|
||||
underline="none"
|
||||
className={classes.introDismissLink}
|
||||
>
|
||||
<CloseIcon className={classes.introDismissIcon} />
|
||||
<Typography component="span" className={classes.introDismissText}>
|
||||
Dismiss
|
||||
</Typography>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type SidebarIntroLocalStorage = {
|
||||
starredItemsDismissed: boolean;
|
||||
recentlyViewedItemsDismissed: boolean;
|
||||
};
|
||||
|
||||
type SidebarIntroCardProps = {
|
||||
text: string;
|
||||
onDismiss: () => void;
|
||||
};
|
||||
|
||||
const SidebarIntroCard: FC<SidebarIntroCardProps> = (props) => {
|
||||
const { text, onDismiss } = props;
|
||||
const [collapsing, setCollapsing] = useState(false);
|
||||
const startDismissing = () => {
|
||||
setCollapsing(true);
|
||||
};
|
||||
return (
|
||||
<Collapse in={!collapsing} onExited={onDismiss}>
|
||||
<IntroCard text={text} onClose={startDismissing} />
|
||||
</Collapse>
|
||||
);
|
||||
};
|
||||
|
||||
const starredIntroText = `Fun fact! As you explore all the awesome plugins in Backstage, you can actually pin them to this side nav.
|
||||
Keep an eye out for the little star icon (⭐) next to the plugin name and give it a click!`;
|
||||
const recentlyViewedIntroText =
|
||||
'And your recently viewed plugins will pop up here!';
|
||||
|
||||
export const SidebarIntro: FC = () => {
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const [
|
||||
{ starredItemsDismissed, recentlyViewedItemsDismissed },
|
||||
setDismissedIntro,
|
||||
] = useLocalStorage<SidebarIntroLocalStorage>(SIDEBAR_INTRO_LOCAL_STORAGE, {
|
||||
starredItemsDismissed: false,
|
||||
recentlyViewedItemsDismissed: false,
|
||||
});
|
||||
|
||||
const dismissStarred = () => {
|
||||
setDismissedIntro((state) => ({ ...state, starredItemsDismissed: true }));
|
||||
};
|
||||
const dismissRecentlyViewed = () => {
|
||||
setDismissedIntro((state) => ({
|
||||
...state,
|
||||
recentlyViewedItemsDismissed: true,
|
||||
}));
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{!starredItemsDismissed && (
|
||||
<>
|
||||
<SidebarIntroCard
|
||||
text={starredIntroText}
|
||||
onDismiss={dismissStarred}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
</>
|
||||
)}
|
||||
{!recentlyViewedItemsDismissed && (
|
||||
<SidebarIntroCard
|
||||
text={recentlyViewedIntroText}
|
||||
onDismiss={dismissRecentlyViewed}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -14,87 +14,196 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Link, makeStyles, styled, Theme, Typography } from '@material-ui/core';
|
||||
import {
|
||||
makeStyles,
|
||||
styled,
|
||||
TextField,
|
||||
Theme,
|
||||
Typography,
|
||||
Badge,
|
||||
} from '@material-ui/core';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
import clsx from 'clsx';
|
||||
import React, { FC, useContext } from 'react';
|
||||
import React, { FC, useContext, useState, KeyboardEventHandler } from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { sidebarConfig, SidebarContext } from './config';
|
||||
import { IconComponent } from '../../icons';
|
||||
|
||||
const useStyles = makeStyles<Theme>((theme) => ({
|
||||
root: {
|
||||
color: '#b5b5b5',
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
alignItems: 'center',
|
||||
height: 40,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
closed: {
|
||||
width: sidebarConfig.drawerWidthClosed,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
open: {
|
||||
width: sidebarConfig.drawerWidthOpen,
|
||||
},
|
||||
label: {
|
||||
fontWeight: 'bolder',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: 1.0,
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
iconContainer: {
|
||||
height: '100%',
|
||||
width: sidebarConfig.drawerWidthClosed,
|
||||
marginRight: -theme.spacing(2),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
}));
|
||||
const useStyles = makeStyles<Theme>((theme) => {
|
||||
const {
|
||||
selectedIndicatorWidth,
|
||||
drawerWidthClosed,
|
||||
drawerWidthOpen,
|
||||
iconContainerWidth,
|
||||
iconSize,
|
||||
} = sidebarConfig;
|
||||
|
||||
return {
|
||||
root: {
|
||||
color: '#b5b5b5',
|
||||
display: 'flex',
|
||||
flexFlow: 'row nowrap',
|
||||
alignItems: 'center',
|
||||
height: 48,
|
||||
cursor: 'pointer',
|
||||
},
|
||||
closed: {
|
||||
width: drawerWidthClosed,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
open: {
|
||||
width: drawerWidthOpen,
|
||||
},
|
||||
label: {
|
||||
// XXX (@koroeskohr): I can't seem to achieve the desired font-weight from the designs
|
||||
fontWeight: 'bold',
|
||||
whiteSpace: 'nowrap',
|
||||
lineHeight: 1.0,
|
||||
},
|
||||
iconContainer: {
|
||||
boxSizing: 'border-box',
|
||||
height: '100%',
|
||||
width: iconContainerWidth,
|
||||
marginRight: -theme.spacing(2),
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
icon: {
|
||||
width: iconSize,
|
||||
height: iconSize,
|
||||
},
|
||||
searchRoot: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
searchField: {
|
||||
color: '#b5b5b5',
|
||||
fontWeight: 'bold',
|
||||
fontSize: theme.typography.fontSize,
|
||||
},
|
||||
searchContainer: {
|
||||
width: drawerWidthOpen - iconContainerWidth,
|
||||
},
|
||||
selected: {
|
||||
'&$root': {
|
||||
borderLeft: `solid ${selectedIndicatorWidth}px #9BF0E1`,
|
||||
color: '#ffffff',
|
||||
},
|
||||
'&$closed': {
|
||||
width: drawerWidthClosed - selectedIndicatorWidth,
|
||||
},
|
||||
'& $iconContainer': {
|
||||
marginLeft: -selectedIndicatorWidth,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type SidebarItemProps = {
|
||||
icon: IconComponent;
|
||||
text: string;
|
||||
text?: string;
|
||||
to?: string;
|
||||
disableSelected?: boolean;
|
||||
hasNotifications?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const SidebarItem: FC<SidebarItemProps> = ({
|
||||
icon: Icon,
|
||||
text,
|
||||
to,
|
||||
to = '#',
|
||||
disableSelected = false,
|
||||
hasNotifications = false,
|
||||
onClick,
|
||||
children,
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const isOpen = useContext(SidebarContext);
|
||||
// XXX (@koroeskohr): unsure this is optimal. But I just really didn't want to have the item component
|
||||
// depend on the current location, and at least have it being optionally forced to selected.
|
||||
// Still waiting on a Q answered to fine tune the implementation
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
|
||||
const itemIcon = (
|
||||
<Badge
|
||||
color="secondary"
|
||||
variant="dot"
|
||||
overlap="circle"
|
||||
invisible={!hasNotifications}
|
||||
>
|
||||
<Icon fontSize="small" className={classes.icon} />
|
||||
</Badge>
|
||||
);
|
||||
|
||||
if (!isOpen) {
|
||||
return (
|
||||
<Link
|
||||
<NavLink
|
||||
className={clsx(classes.root, classes.closed)}
|
||||
href={to}
|
||||
activeClassName={classes.selected}
|
||||
isActive={(match) => Boolean(match && !disableSelected)}
|
||||
exact
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
underline="none"
|
||||
>
|
||||
<Icon fontSize="small" />
|
||||
</Link>
|
||||
{itemIcon}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
<NavLink
|
||||
className={clsx(classes.root, classes.open)}
|
||||
href={to}
|
||||
activeClassName={classes.selected}
|
||||
isActive={(match) => Boolean(match && !disableSelected)}
|
||||
exact
|
||||
to={to}
|
||||
onClick={onClick}
|
||||
underline="none"
|
||||
>
|
||||
<div data-testid="login-button" className={classes.iconContainer}>
|
||||
<Icon fontSize="small" />
|
||||
{itemIcon}
|
||||
</div>
|
||||
<Typography variant="subtitle1" className={classes.label}>
|
||||
{text}
|
||||
</Typography>
|
||||
</Link>
|
||||
{text && (
|
||||
<Typography variant="subtitle2" className={classes.label}>
|
||||
{text}
|
||||
</Typography>
|
||||
)}
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
type SidebarSearchFieldProps = {
|
||||
onSearch: (input: string) => void;
|
||||
};
|
||||
|
||||
export const SidebarSearchField: FC<SidebarSearchFieldProps> = (props) => {
|
||||
const [input, setInput] = useState('');
|
||||
const classes = useStyles();
|
||||
|
||||
const handleEnter: KeyboardEventHandler = (ev) => {
|
||||
if (ev.key === 'Enter') {
|
||||
props.onSearch(input);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInput = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setInput(ev.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.searchRoot}>
|
||||
<SidebarItem icon={SearchIcon} disableSelected>
|
||||
<TextField
|
||||
placeholder="Search"
|
||||
onChange={handleInput}
|
||||
onKeyDown={handleEnter}
|
||||
className={classes.searchContainer}
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
className: classes.searchField,
|
||||
}}
|
||||
/>
|
||||
</SidebarItem>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -111,4 +220,5 @@ export const SidebarDivider = styled('hr')({
|
||||
width: '100%',
|
||||
background: '#383838',
|
||||
border: 'none',
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarIntro,
|
||||
SidebarItem,
|
||||
SidebarDivider,
|
||||
SidebarSearchField,
|
||||
SidebarSpace,
|
||||
SidebarUserBadge,
|
||||
} from '.';
|
||||
import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined';
|
||||
import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
export default {
|
||||
title: 'Sidebar',
|
||||
component: Sidebar,
|
||||
decorators: [
|
||||
(storyFn: () => JSX.Element) => (
|
||||
<MemoryRouter initialEntries={['/']}>{storyFn()}</MemoryRouter>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
const handleSearch = (input: string) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(input);
|
||||
};
|
||||
|
||||
export const SampleSidebar = () => (
|
||||
<Sidebar>
|
||||
{/* <SidebarLogo /> */}
|
||||
<SidebarSearchField onSearch={handleSearch} />
|
||||
<SidebarDivider />
|
||||
<SidebarItem icon={HomeOutlinedIcon} to="#" text="Home" />
|
||||
<SidebarItem icon={HomeOutlinedIcon} to="#" text="Plugins" />
|
||||
<SidebarItem icon={AddCircleOutlineIcon} to="#" text="Create..." />
|
||||
<SidebarDivider />
|
||||
<SidebarIntro />
|
||||
<SidebarSpace />
|
||||
<SidebarDivider />
|
||||
<SidebarUserBadge />
|
||||
</Sidebar>
|
||||
);
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2020 Spotify AB
|
||||
*
|
||||
* 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 React, { FC, useContext } from 'react';
|
||||
import { Avatar, makeStyles, Theme, Typography } from '@material-ui/core';
|
||||
import People from '@material-ui/icons/People';
|
||||
import { sidebarConfig, SidebarContext } from './config';
|
||||
import { SidebarItem } from './Items';
|
||||
|
||||
const useStyles = makeStyles<Theme>(() => {
|
||||
const { drawerWidthOpen, userBadgeDiameter } = sidebarConfig;
|
||||
return {
|
||||
root: {
|
||||
width: drawerWidthOpen,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
color: '#b5b5b5',
|
||||
paddingLeft: 18,
|
||||
paddingTop: 14,
|
||||
paddingBottom: 14,
|
||||
},
|
||||
avatar: {
|
||||
width: userBadgeDiameter,
|
||||
height: userBadgeDiameter,
|
||||
marginRight: 8,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type Props = {
|
||||
imageUrl: string;
|
||||
name: string;
|
||||
hideName?: boolean;
|
||||
};
|
||||
|
||||
export const UserBadge: FC<Props> = ({ imageUrl, name, hideName = false }) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Avatar alt={name} src={imageUrl} className={classes.avatar} />
|
||||
{!hideName && <Typography variant="subtitle2">{name}</Typography>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SidebarUserBadge: FC = () => {
|
||||
const { isOpen } = useContext(SidebarContext);
|
||||
const isUserLoggedIn = false;
|
||||
return isUserLoggedIn ? (
|
||||
<UserBadge
|
||||
imageUrl="https://via.placeholder.com/200/200"
|
||||
name="Victor Viale"
|
||||
hideName={!isOpen}
|
||||
/>
|
||||
) : (
|
||||
<SidebarItem icon={People} text="Log in" to="/login" disableSelected />
|
||||
);
|
||||
};
|
||||
@@ -16,11 +16,34 @@
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
const drawerWidthClosed = 72;
|
||||
const iconPadding = 24;
|
||||
const userBadgePadding = 18;
|
||||
|
||||
export const sidebarConfig = {
|
||||
drawerWidthClosed: 64,
|
||||
drawerWidthOpen: 220,
|
||||
defaultOpenDelayMs: 400,
|
||||
defaultCloseDelayMs: 200,
|
||||
drawerWidthClosed,
|
||||
drawerWidthOpen: 224,
|
||||
// As per NN/g's guidance on timing for exposing hidden content
|
||||
// See https://www.nngroup.com/articles/timing-exposing-content/
|
||||
defaultOpenDelayMs: 300,
|
||||
defaultCloseDelayMs: 0,
|
||||
defaultFadeDuration: 200,
|
||||
logoHeight: 32,
|
||||
iconContainerWidth: drawerWidthClosed,
|
||||
iconSize: drawerWidthClosed - iconPadding * 2,
|
||||
iconPadding,
|
||||
selectedIndicatorWidth: 3,
|
||||
userBadgePadding,
|
||||
userBadgeDiameter: drawerWidthClosed - userBadgePadding * 2,
|
||||
};
|
||||
|
||||
export const SidebarContext = createContext<boolean>(false);
|
||||
export const SIDEBAR_INTRO_LOCAL_STORAGE =
|
||||
'@backstage/core/sidebar-intro-dismissed';
|
||||
|
||||
export type SidebarContextType = {
|
||||
isOpen: boolean;
|
||||
};
|
||||
|
||||
export const SidebarContext = createContext<SidebarContextType>({
|
||||
isOpen: false,
|
||||
});
|
||||
|
||||
@@ -17,5 +17,7 @@
|
||||
export * from './Bar';
|
||||
export * from './Page';
|
||||
export * from './Items';
|
||||
export * from './Intro';
|
||||
export * from './UserBadge';
|
||||
export * from './config';
|
||||
export { SidebarThemeToggle } from './SidebarThemeToggle';
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
@@ -35,11 +36,12 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0"
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { hot } from 'react-hot-loader/root';
|
||||
import React, { FC, ComponentType } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
@@ -90,10 +91,10 @@ class DevAppBuilder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and render directory to #root element
|
||||
* Build and render directory to #root element, with react hot loading.
|
||||
*/
|
||||
render(): void {
|
||||
const DevApp = this.build();
|
||||
const DevApp = hot(this.build());
|
||||
|
||||
const paths = this.findPluginPaths(this.plugins);
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -15,15 +15,10 @@ module.exports = {
|
||||
webpackFinal: async (config) => {
|
||||
const coreSrc = path.resolve(__dirname, '../../core/src');
|
||||
|
||||
config.resolve.alias = {
|
||||
...config.resolve.alias,
|
||||
// Resolves imports of @backstage/core inside the storybook config, pointing to src
|
||||
'@backstage/core': coreSrc,
|
||||
// Point to dist version of theme and any other packages that might be needed in the future
|
||||
'@backstage/theme': path.resolve(__dirname, '../../theme'),
|
||||
};
|
||||
// Mirror config in packages/cli/src/lib/bundler
|
||||
config.resolve.mainFields = ['main:src', 'browser', 'module', 'main'];
|
||||
|
||||
// Remove the default babel-loader for js files, we're using ts-loader instead
|
||||
// Remove the default babel-loader for js files, we're using sucrase instead
|
||||
const [jsLoader] = config.module.rules.splice(0, 1);
|
||||
if (jsLoader.use[0].loader !== 'babel-loader') {
|
||||
throw new Error(
|
||||
@@ -33,20 +28,24 @@ module.exports = {
|
||||
|
||||
config.resolve.extensions.push('.ts', '.tsx');
|
||||
|
||||
// Use ts-loader for all JS/TS files
|
||||
config.module.rules.push({
|
||||
test: /\.(ts|tsx|mjs|js|jsx)$/,
|
||||
include: [__dirname, coreSrc],
|
||||
exclude: /node_modules/,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('ts-loader'),
|
||||
options: {
|
||||
transpileOnly: true,
|
||||
},
|
||||
config.module.rules.push(
|
||||
{
|
||||
test: /\.(tsx?)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: require.resolve('@sucrase/webpack-loader'),
|
||||
options: {
|
||||
transforms: ['typescript', 'jsx', 'react-hot-loader'],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
{
|
||||
test: /\.(jsx?|mjs)$/,
|
||||
exclude: /node_modules/,
|
||||
loader: require.resolve('@sucrase/webpack-loader'),
|
||||
options: {
|
||||
transforms: ['jsx', 'react-hot-loader'],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Disable ProgressPlugin which logs verbose webpack build progress. Warnings and Errors are still logged.
|
||||
config.plugins = config.plugins.filter(
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
"description": "Storybook build for core package",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "backstage-cli watch-deps --build -- start-storybook -p 6006",
|
||||
"build-storybook": "backstage-cli watch-deps --build -- build-storybook --output-dir dist"
|
||||
"start": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook --output-dir dist"
|
||||
},
|
||||
"workspaces": {
|
||||
"nohoist": [
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": [".storybook/**/*"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
@@ -33,7 +34,7 @@
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"react": "^16.12.0",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
# patches
|
||||
|
||||
This folder contains patches for dependency type declarations. Typescript doesn't provide a way to override of fix types that are installed as a part of the dependent package itself.
|
||||
|
||||
Do not add any more patches here, these patches were added as a part of getting the entire repo type-checked, and we depended on some packages with bad types.
|
||||
|
||||
As soon as the below issues are fixed, we should remove the patching functionality completely.
|
||||
|
||||
## graphiql
|
||||
|
||||
Some bad TS config in the alpha release - tracked here: https://github.com/graphql/graphiql/issues/1530
|
||||
|
||||
## material-table
|
||||
|
||||
Fixed in master but not released yet, tracked here: https://github.com/mbrn/material-table/pull/1624
|
||||
@@ -0,0 +1,49 @@
|
||||
diff --git a/node_modules/graphiql/dist/components/HistoryQuery.d.ts b/node_modules/graphiql/dist/components/HistoryQuery.d.ts
|
||||
index c903bde..761b76b 100644
|
||||
--- a/node_modules/graphiql/dist/components/HistoryQuery.d.ts
|
||||
+++ b/node_modules/graphiql/dist/components/HistoryQuery.d.ts
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
-import { QueryStoreItem } from 'src/utility/QueryStore';
|
||||
+import { QueryStoreItem } from '../utility/QueryStore';
|
||||
export declare type HandleEditLabelFn = (query?: string, variables?: string, operationName?: string, label?: string, favorite?: boolean) => void;
|
||||
export declare type HandleToggleFavoriteFn = (query?: string, variables?: string, operationName?: string, label?: string, favorite?: boolean) => void;
|
||||
export declare type HandleSelectQueryFn = (query?: string, variables?: string, operationName?: string, label?: string) => void;
|
||||
diff --git a/node_modules/graphiql/dist/components/QueryEditor.d.ts b/node_modules/graphiql/dist/components/QueryEditor.d.ts
|
||||
index b508e92..35e7fb7 100644
|
||||
--- a/node_modules/graphiql/dist/components/QueryEditor.d.ts
|
||||
+++ b/node_modules/graphiql/dist/components/QueryEditor.d.ts
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import * as CM from 'codemirror';
|
||||
import { GraphQLSchema, GraphQLType } from 'graphql';
|
||||
-import { SizerComponent } from 'src/utility/CodeMirrorSizer';
|
||||
+import { SizerComponent } from '../utility/CodeMirrorSizer';
|
||||
declare type QueryEditorProps = {
|
||||
schema?: GraphQLSchema;
|
||||
value?: string;
|
||||
diff --git a/node_modules/graphiql/dist/components/QueryHistory.d.ts b/node_modules/graphiql/dist/components/QueryHistory.d.ts
|
||||
index 409af29..9362657 100644
|
||||
--- a/node_modules/graphiql/dist/components/QueryHistory.d.ts
|
||||
+++ b/node_modules/graphiql/dist/components/QueryHistory.d.ts
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import QueryStore, { QueryStoreItem } from '../utility/QueryStore';
|
||||
import { HandleEditLabelFn, HandleToggleFavoriteFn, HandleSelectQueryFn } from './HistoryQuery';
|
||||
-import StorageAPI from 'src/utility/StorageAPI';
|
||||
+import StorageAPI from '../utility/StorageAPI';
|
||||
declare type QueryHistoryProps = {
|
||||
query?: string;
|
||||
variables?: string;
|
||||
diff --git a/node_modules/graphiql/dist/components/ResultViewer.d.ts b/node_modules/graphiql/dist/components/ResultViewer.d.ts
|
||||
index 55976f5..11b725a 100644
|
||||
--- a/node_modules/graphiql/dist/components/ResultViewer.d.ts
|
||||
+++ b/node_modules/graphiql/dist/components/ResultViewer.d.ts
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component, FunctionComponent } from 'react';
|
||||
import * as CM from 'codemirror';
|
||||
-import { SizerComponent } from 'src/utility/CodeMirrorSizer';
|
||||
+import { SizerComponent } from '../utility/CodeMirrorSizer';
|
||||
import { ImagePreview as ImagePreviewComponent } from './ImagePreview';
|
||||
declare type ResultViewerProps = {
|
||||
value?: string;
|
||||
@@ -0,0 +1,12 @@
|
||||
diff --git a/node_modules/material-table/types/index.d.ts b/node_modules/material-table/types/index.d.ts
|
||||
index 06b700b..5b3c765 100644
|
||||
--- a/node_modules/material-table/types/index.d.ts
|
||||
+++ b/node_modules/material-table/types/index.d.ts
|
||||
@@ -228,7 +228,6 @@ export interface Options {
|
||||
showTitle?: boolean;
|
||||
showTextRowsSelected?: boolean;
|
||||
search?: boolean;
|
||||
- searchText?: string;
|
||||
searchFieldAlignment?: 'left' | 'right';
|
||||
searchFieldStyle?: React.CSSProperties;
|
||||
searchText?: string;
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@backstage/plugin-auth-backend",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -9,6 +10,8 @@
|
||||
"build": "tsc",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -28,7 +28,7 @@ export async function createRouter(
|
||||
const logger = options.logger.child({ plugin: 'auth' });
|
||||
const router = Router();
|
||||
|
||||
router.get('/ping', async (req, res) => {
|
||||
router.get('/ping', async (_req, res) => {
|
||||
res.status(200).send('pong');
|
||||
});
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@backstage/plugin-catalog-backend",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -9,6 +10,8 @@
|
||||
"build": "tsc",
|
||||
"lint": "backstage-cli lint",
|
||||
"test": "backstage-cli test",
|
||||
"prepack": "backstage-cli prepack",
|
||||
"postpack": "backstage-cli postpack",
|
||||
"clean": "backstage-cli clean"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function createRouter(
|
||||
if (itemsCatalog) {
|
||||
// Components
|
||||
router
|
||||
.get('/components', async (req, res) => {
|
||||
.get('/components', async (_req, res) => {
|
||||
const components = await itemsCatalog.components();
|
||||
res.status(200).send(components);
|
||||
})
|
||||
@@ -55,7 +55,7 @@ export async function createRouter(
|
||||
const output = await locationsCatalog.addLocation(input);
|
||||
res.status(201).send(output);
|
||||
})
|
||||
.get('/locations', async (req, res) => {
|
||||
.get('/locations', async (_req, res) => {
|
||||
const output = await locationsCatalog.locations();
|
||||
res.status(200).send(output);
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@backstage/plugin-catalog",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
@@ -31,7 +32,7 @@
|
||||
"@backstage/test-utils": "^0.1.1-alpha.5",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "dev"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@backstage/plugin-explore",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
@@ -32,7 +33,7 @@
|
||||
"@backstage/test-utils": "^0.1.1-alpha.5",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "dev"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"scripts": {
|
||||
"build": "backstage-cli plugin:build",
|
||||
@@ -46,7 +47,8 @@
|
||||
"@backstage/test-utils": "^0.1.1-alpha.5",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/codemirror": "^0.0.93",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ApiProvider, ApiRegistry } from '@backstage/core';
|
||||
import { renderWithEffects } from '@backstage/test-utils';
|
||||
import { GraphQLBrowseApi, graphQlBrowseApiRef } from '../../lib/api';
|
||||
|
||||
jest.mock('components/GraphiQLBrowser', () => ({
|
||||
jest.mock('../GraphiQLBrowser', () => ({
|
||||
GraphiQLBrowser: () => '<GraphiQLBrowser />',
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "dev"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@backstage/plugin-home-page",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
@@ -30,7 +31,7 @@
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.5",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "dev"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@backstage/plugin-lighthouse",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
@@ -33,7 +34,7 @@
|
||||
"@backstage/test-utils": "^0.1.1-alpha.5",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "dev"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@backstage/plugin-register-component",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
@@ -31,7 +32,7 @@
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.5",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@backstage/plugin-scaffolder",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
@@ -30,7 +31,7 @@
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.5",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/testing-library__jest-dom": "^5.0.4",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["src", "dev"],
|
||||
"compilerOptions": {}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
"name": "@backstage/plugin-tech-radar",
|
||||
"version": "0.1.1-alpha.5",
|
||||
"main": "dist/index.esm.js",
|
||||
"main:src": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
@@ -34,7 +35,7 @@
|
||||
"@backstage/dev-utils": "^0.1.1-alpha.5",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@testing-library/user-event": "^10.2.4",
|
||||
"@types/color": "^3.0.1",
|
||||
"@types/d3-force": "^1.2.1",
|
||||
"@types/jest": "^25.2.1",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user