Merge remote-tracking branch 'upstream/master'
This commit is contained in:
+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": {}
|
||||
}
|
||||
Reference in New Issue
Block a user