Merge pull request #895 from spotify/rugvip/build

New Build Setup
This commit is contained in:
Patrik Oldsberg
2020-05-18 10:59:29 +02:00
committed by GitHub
95 changed files with 1434 additions and 2561 deletions
+1
View File
@@ -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 }}
+15 -7
View File
@@ -56,14 +56,19 @@ 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' }}
run: yarn lerna -- run build --since origin/master
- 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 +78,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
+3
View File
@@ -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
+4 -3
View File
@@ -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.
+9
View File
@@ -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
View File
@@ -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": {
@@ -33,6 +35,7 @@
"husky": "^4.2.3",
"lerna": "^3.20.2",
"lint-staged": "^10.1.0",
"patch-package": "^6.2.2",
"prettier": "^2.0.5"
},
"husky": {
-21
View File
@@ -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"]
}
+17
View File
@@ -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.
*/
declare module 'rollup-plugin-esbuild';
+44 -21
View File
@@ -14,44 +14,67 @@
* 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'),
},
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 (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();
+2
View File
@@ -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"],
+16 -2
View File
@@ -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",
+9 -9
View File
@@ -14,15 +14,15 @@
* 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,
});
// Wait for interrupt signal
await new Promise(() => {});
};
+9 -14
View File
@@ -14,20 +14,15 @@
* 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) => {
await serveBundle({
entry: 'src/index',
checksEnabled: cmd.check,
});
// Wait for interrupt signal
await new Promise(() => {});
};
@@ -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);
+3 -80
View File
@@ -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;
@@ -14,13 +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 });
await startDevServer();
export default async (cmd: Command) => {
await serveBundle({
entry: 'dev/index',
checksEnabled: cmd.check,
});
// Wait for interrupt signal
await new Promise(() => {});
+4 -2
View File
@@ -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
@@ -155,7 +157,7 @@ function actionHandler<T extends readonly any[]>(
};
}
process.on('unhandledRejection', (rejection) => {
process.on('unhandledRejection', rejection => {
if (rejection instanceof Error) {
exitWithError(rejection);
} else {
+112
View File
@@ -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 };
}
@@ -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',
+18
View File
@@ -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,
},
},
},
};
};
@@ -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>;
@@ -14,14 +14,16 @@
* 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;
@@ -30,17 +32,19 @@ export async function startDevServer() {
return;
}
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,
@@ -53,6 +57,13 @@ export async function startDevServer() {
return;
}
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, () => {
server.close();
process.exit();
});
}
openBrowser(urls.localUrlForBrowser);
resolve();
});
+101
View File
@@ -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 };
};
+30
View File
@@ -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;
};
+81
View File
@@ -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()],
},
];
};
+17
View File
@@ -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';
+87
View File
@@ -0,0 +1,87 @@
/*
* 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') {
// typescript2 plugin has a complete message with all codeframes
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));
};
+97 -4
View File
@@ -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,92 @@ 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',
];
export async function installWithLocalDeps(dir: string) {
// e2e testing needs special treatment
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')}`,
);
});
});
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',
);
console.log('DEBUG: depJsonPath =', depJsonPath);
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"
}
}
@@ -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,
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "dev"],
"compilerOptions": {}
}
+4 -1
View File
@@ -1,8 +1,11 @@
{
"extends": "./tsconfig.json",
"extends": "./config/tsconfig.json",
"include": ["src"],
"exclude": ["**/*.test.*"],
"compilerOptions": {
"outDir": "dist",
"emitDeclarationOnly": false,
"removeComments": true,
"module": "CommonJS"
}
}
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"compilerOptions": {}
}
+9 -8
View File
@@ -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": "^7.1.2"
},
"files": [
"dist/**/*.{js,d.ts}"
+1 -1
View File
@@ -14,5 +14,5 @@
* limitations under the License.
*/
export type { NavTarget, NavTargetConfig, NavTargetOverrideConfig } from './types';
export * from './types';
export { createNavTarget } from './NavTarget';
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"compilerOptions": {}
}
+2
View File
@@ -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",
@@ -40,6 +41,7 @@
"@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"
},
+3 -2
View File
@@ -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);
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"compilerOptions": {}
}
+20 -21
View File
@@ -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(
+2 -2
View File
@@ -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": [
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": [".storybook/**/*"],
"compilerOptions": {}
}
+1
View File
@@ -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",
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"compilerOptions": {}
}
+1
View File
@@ -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",
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"compilerOptions": {}
}
+1
View File
@@ -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",
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"compilerOptions": {}
}
+15
View File
@@ -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
+49
View File
@@ -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;
+12
View File
@@ -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;
+3
View File
@@ -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": {
+1 -1
View File
@@ -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');
});
+3
View File
@@ -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);
})
+1
View File
@@ -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,
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "dev"],
"compilerOptions": {}
}
+1
View File
@@ -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,
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "dev"],
"compilerOptions": {}
}
+2
View File
@@ -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",
@@ -47,6 +48,7 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@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 />',
}));
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "dev"],
"compilerOptions": {}
}
+1
View File
@@ -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,
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "dev"],
"compilerOptions": {}
}
+1
View File
@@ -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,
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "dev"],
"compilerOptions": {}
}
+1
View File
@@ -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,
+1
View File
@@ -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,
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "dev"],
"compilerOptions": {}
}
+1
View File
@@ -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,
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "dev"],
"compilerOptions": {}
}
+1
View File
@@ -2,6 +2,7 @@
"name": "@backstage/plugin-welcome",
"version": "0.1.1-alpha.5",
"main": "dist/index.esm.js",
"main:src": "src/index.ts",
"types": "src/index.ts",
"private": true,
"license": "Apache-2.0",
-5
View File
@@ -1,5 +0,0 @@
{
"extends": "../../tsconfig.json",
"include": ["src", "dev"],
"compilerOptions": {}
}
+6 -1
View File
@@ -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"
}
}
+352 -2054
View File
File diff suppressed because it is too large Load Diff