@backstage/cli
Version:
CLI for developing Backstage plugins and apps
725 lines (713 loc) • 23 kB
JavaScript
;
var path = require('path');
var webpack = require('webpack');
var ESLintPlugin = require('eslint-webpack-plugin');
var ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var cliCommon = require('@backstage/cli-common');
var ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin');
var runScriptWebpackPlugin = require('run-script-webpack-plugin');
var index = require('./index-3decf946.cjs.js');
var fs = require('fs-extra');
var getPackages = require('@manypkg/get-packages');
var nodeExternals = require('webpack-node-externals');
var pickBy = require('lodash/pickBy');
var entryPoints = require('./entryPoints-0cc55995.cjs.js');
var run = require('./run-168542e8.cjs.js');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var svgrTemplate = require('./svgrTemplate-3549ea1c.cjs.js');
var ReactRefreshPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
var yn = require('yn');
var config = require('@backstage/config');
var chokidar = require('chokidar');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var webpack__default = /*#__PURE__*/_interopDefaultLegacy(webpack);
var ESLintPlugin__default = /*#__PURE__*/_interopDefaultLegacy(ESLintPlugin);
var ForkTsCheckerWebpackPlugin__default = /*#__PURE__*/_interopDefaultLegacy(ForkTsCheckerWebpackPlugin);
var HtmlWebpackPlugin__default = /*#__PURE__*/_interopDefaultLegacy(HtmlWebpackPlugin);
var ModuleScopePlugin__default = /*#__PURE__*/_interopDefaultLegacy(ModuleScopePlugin);
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
var nodeExternals__default = /*#__PURE__*/_interopDefaultLegacy(nodeExternals);
var pickBy__default = /*#__PURE__*/_interopDefaultLegacy(pickBy);
var MiniCssExtractPlugin__default = /*#__PURE__*/_interopDefaultLegacy(MiniCssExtractPlugin);
var ReactRefreshPlugin__default = /*#__PURE__*/_interopDefaultLegacy(ReactRefreshPlugin);
var yn__default = /*#__PURE__*/_interopDefaultLegacy(yn);
var chokidar__default = /*#__PURE__*/_interopDefaultLegacy(chokidar);
class LinkedPackageResolvePlugin {
constructor(targetModules, packages) {
this.targetModules = targetModules;
this.packages = packages;
}
apply(resolver) {
resolver.hooks.resolve.tapAsync(
"LinkedPackageResolvePlugin",
(data, context, callback) => {
var _a;
const pkg = this.packages.find(
(pkge) => data.path && cliCommon.isChildPath(pkge.dir, data.path)
);
if (!pkg) {
callback();
return;
}
const modulesLocation = path.resolve(
this.targetModules,
pkg.packageJson.name
);
const newContext = ((_a = data.context) == null ? void 0 : _a.issuer) ? {
...data.context,
issuer: data.context.issuer.replace(pkg.dir, modulesLocation)
} : data.context;
resolver.doResolve(
resolver.hooks.resolve,
{
...data,
context: newContext,
path: data.path && data.path.replace(pkg.dir, modulesLocation)
},
`resolve ${data.request} in ${modulesLocation}`,
context,
callback
);
}
);
}
}
const { ESBuildMinifyPlugin } = require("esbuild-loader");
const optimization = (options) => {
const { isDev } = options;
return {
minimize: !isDev,
minimizer: [
new ESBuildMinifyPlugin({
target: "es2019",
format: "iife"
})
],
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(module) {
var _a;
return Boolean(
(_a = module == null ? void 0 : module.resource) == null ? void 0 : _a.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)
);
},
name(module) {
const packageName = module.resource.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
return packageName.replace("@", "");
},
filename: isDev ? "module-[name].js" : "static/module-[name].[chunkhash:8].js",
priority: 10,
minSize: 1e5,
minChunks: 1,
maxAsyncRequests: Infinity,
maxInitialRequests: Infinity
},
// 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
}
}
}
};
};
const transforms = (options) => {
const { isDev, isBackend } = options;
function insertBeforeJssStyles(element) {
const head = document.head;
const firstJssNode = head.querySelector("style[data-jss]");
if (!firstJssNode) {
head.appendChild(element);
} else {
head.insertBefore(element, firstJssNode);
}
}
const loaders = [
{
test: /\.(tsx?)$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve("swc-loader"),
options: {
jsc: {
target: "es2019",
externalHelpers: !isBackend,
parser: {
syntax: "typescript",
tsx: !isBackend,
dynamicImport: true
},
transform: {
react: isBackend ? void 0 : {
runtime: "automatic",
refresh: isDev
}
}
}
}
}
]
},
{
test: /\.(jsx?|mjs|cjs)$/,
exclude: /node_modules/,
use: [
{
loader: require.resolve("swc-loader"),
options: {
jsc: {
target: "es2019",
externalHelpers: !isBackend,
parser: {
syntax: "ecmascript",
jsx: !isBackend,
dynamicImport: true
},
transform: {
react: isBackend ? void 0 : {
runtime: "automatic",
refresh: isDev
}
}
}
}
}
]
},
{
test: /\.(js|mjs|cjs)$/,
resolve: {
fullySpecified: false
}
},
{
test: [/\.icon\.svg$/],
use: [
{
loader: require.resolve("swc-loader"),
options: {
jsc: {
target: "es2019",
externalHelpers: !isBackend,
parser: {
syntax: "ecmascript",
jsx: !isBackend,
dynamicImport: true
}
}
}
},
{
loader: require.resolve("@svgr/webpack"),
options: { babel: false, template: svgrTemplate.svgrTemplate }
}
]
},
{
test: [
/\.bmp$/,
/\.gif$/,
/\.jpe?g$/,
/\.png$/,
/\.frag$/,
/\.vert$/,
{ and: [/\.svg$/, { not: [/\.icon\.svg$/] }] },
/\.xml$/
],
type: "asset/resource",
generator: {
filename: "static/[name].[hash:8].[ext]"
}
},
{
test: /\.(eot|woff|woff2|ttf)$/i,
type: "asset/resource",
generator: {
filename: "static/[name].[hash][ext][query]"
}
},
{
test: /\.ya?ml$/,
use: require.resolve("yml-loader")
},
{
include: /\.(md)$/,
type: "asset/resource",
generator: {
filename: "static/[name].[hash][ext][query]"
}
},
{
test: /\.css$/i,
use: [
isDev ? {
loader: require.resolve("style-loader"),
options: {
insert: insertBeforeJssStyles
}
} : MiniCssExtractPlugin__default["default"].loader,
{
loader: require.resolve("css-loader"),
options: {
sourceMap: true
}
}
]
}
];
const plugins = new Array();
if (isDev) {
if (!isBackend) {
plugins.push(
new ReactRefreshPlugin__default["default"]({
overlay: { sockProtocol: "ws" }
})
);
}
} else {
plugins.push(
new MiniCssExtractPlugin__default["default"]({
filename: "static/[name].[contenthash:8].css",
chunkFilename: "static/[name].[id].[contenthash:8].css",
insert: insertBeforeJssStyles
// Only applies to async chunks
})
);
}
return { loaders, plugins };
};
function hasReactDomClient() {
try {
require.resolve("react-dom/client");
return true;
} catch {
return false;
}
}
const BUILD_CACHE_ENV_VAR = "BACKSTAGE_CLI_EXPERIMENTAL_BUILD_CACHE";
function resolveBaseUrl(config) {
const baseUrl = config.getString("app.baseUrl");
try {
return new URL(baseUrl);
} catch (error) {
throw new Error(`Invalid app.baseUrl, ${error}`);
}
}
async function readBuildInfo() {
const timestamp = Date.now();
let commit = "unknown";
try {
commit = await run.runPlain("git", "rev-parse", "HEAD");
} catch (error) {
console.warn(`WARNING: Failed to read git commit, ${error}`);
}
let gitVersion = "unknown";
try {
gitVersion = await run.runPlain("git", "describe", "--always");
} catch (error) {
console.warn(`WARNING: Failed to describe git version, ${error}`);
}
const { version: packageVersion } = await fs__default["default"].readJson(
index.paths.resolveTarget("package.json")
);
return {
cliVersion: index.version,
gitVersion,
packageVersion,
timestamp,
commit
};
}
async function createConfig(paths, options) {
var _a;
const { checksEnabled, isDev, frontendConfig } = options;
const { plugins, loaders } = transforms(options);
const { packages } = await getPackages.getPackages(index.paths.targetDir);
const externalPkgs = packages.filter((p) => !cliCommon.isChildPath(paths.root, p.dir));
const baseUrl = frontendConfig.getString("app.baseUrl");
const validBaseUrl = new URL(baseUrl);
const publicPath = validBaseUrl.pathname.replace(/\/$/, "");
if (checksEnabled) {
plugins.push(
new ForkTsCheckerWebpackPlugin__default["default"]({
typescript: { configFile: paths.targetTsConfig, memoryLimit: 4096 }
}),
new ESLintPlugin__default["default"]({
context: paths.targetPath,
files: ["**/*.(ts|tsx|mts|cts|js|jsx|mjs|cjs)"]
})
);
}
plugins.push(
new webpack.ProvidePlugin({
process: "process/browser",
Buffer: ["buffer", "Buffer"]
})
);
plugins.push(
new HtmlWebpackPlugin__default["default"]({
template: paths.targetHtml,
templateParameters: {
publicPath,
config: frontendConfig
}
})
);
const buildInfo = await readBuildInfo();
plugins.push(
new webpack__default["default"].DefinePlugin({
"process.env.BUILD_INFO": JSON.stringify(buildInfo),
"process.env.APP_CONFIG": webpack__default["default"].DefinePlugin.runtimeValue(
() => JSON.stringify(options.getFrontendAppConfigs()),
true
),
// This allows for conditional imports of react-dom/client, since there's no way
// to check for presence of it in source code without module resolution errors.
"process.env.HAS_REACT_DOM_CLIENT": JSON.stringify(hasReactDomClient())
})
);
const reactRefreshFiles = [
require.resolve(
"@pmmmwh/react-refresh-webpack-plugin/lib/runtime/RefreshUtils.js"
),
require.resolve("@pmmmwh/react-refresh-webpack-plugin/overlay/index.js"),
require.resolve("react-refresh")
];
const withCache = yn__default["default"](process.env[BUILD_CACHE_ENV_VAR], { default: false });
return {
mode: isDev ? "development" : "production",
profile: false,
optimization: optimization(options),
bail: false,
performance: {
hints: false
// we check the gzip size instead
},
devtool: isDev ? "eval-cheap-module-source-map" : "source-map",
context: paths.targetPath,
entry: [...(_a = options.additionalEntryPoints) != null ? _a : [], paths.targetEntry],
resolve: {
extensions: [".ts", ".tsx", ".mjs", ".js", ".jsx", ".json", ".wasm"],
mainFields: ["browser", "module", "main"],
fallback: {
...pickBy__default["default"](require("node-libs-browser")),
module: false,
dgram: false,
dns: false,
fs: false,
http2: false,
net: false,
tls: false,
child_process: false,
/* new ignores */
path: false,
https: false,
http: false,
util: require.resolve("util/")
},
plugins: [
new LinkedPackageResolvePlugin(paths.rootNodeModules, externalPkgs),
new ModuleScopePlugin__default["default"](
[paths.targetSrc, paths.targetDev],
[paths.targetPackageJson, ...reactRefreshFiles]
)
]
},
module: {
rules: loaders
},
output: {
path: paths.targetDist,
publicPath: `${publicPath}/`,
filename: isDev ? "[name].js" : "static/[name].[fullhash:8].js",
chunkFilename: isDev ? "[name].chunk.js" : "static/[name].[chunkhash:8].chunk.js",
...isDev ? {
devtoolModuleFilenameTemplate: (info) => `file:///${path.resolve(info.absoluteResourcePath).replace(
/\\/g,
"/"
)}`
} : {}
},
plugins,
...withCache ? {
cache: {
type: "filesystem",
buildDependencies: {
config: [__filename]
}
}
} : {}
};
}
async function createBackendConfig(paths, options) {
const { checksEnabled, isDev } = options;
const { packages } = await getPackages.getPackages(index.paths.targetDir);
const localPackageEntryPoints = packages.flatMap((p) => {
const entryPoints$1 = entryPoints.readEntryPoints(p.packageJson);
return entryPoints$1.map((e) => path.posix.join(p.packageJson.name, e.mount));
});
const moduleDirs = packages.map((p) => path.resolve(p.dir, "node_modules"));
const externalPkgs = packages.filter((p) => !cliCommon.isChildPath(paths.root, p.dir));
const { loaders } = transforms({ ...options, isBackend: true });
const runScriptNodeArgs = new Array();
if (options.inspectEnabled) {
const inspect = typeof options.inspectEnabled === "string" ? `--inspect=${options.inspectEnabled}` : "--inspect";
runScriptNodeArgs.push(inspect);
} else if (options.inspectBrkEnabled) {
const inspect = typeof options.inspectBrkEnabled === "string" ? `--inspect-brk=${options.inspectBrkEnabled}` : "--inspect-brk";
runScriptNodeArgs.push(inspect);
}
return {
mode: isDev ? "development" : "production",
profile: false,
...isDev ? {
watch: true,
watchOptions: {
ignored: /node_modules\/(?!\@backstage)/
}
} : {},
externals: [
nodeExternalsWithResolve({
modulesDir: paths.rootNodeModules,
additionalModuleDirs: moduleDirs,
allowlist: ["webpack/hot/poll?100", ...localPackageEntryPoints]
})
],
target: "node",
node: {
/* eslint-disable-next-line no-restricted-syntax */
__dirname: true,
__filename: true,
global: true
},
bail: false,
performance: {
hints: false
// we check the gzip size instead
},
devtool: isDev ? "eval-cheap-module-source-map" : "source-map",
context: paths.targetPath,
entry: [
"webpack/hot/poll?100",
paths.targetRunFile ? paths.targetRunFile : paths.targetEntry
],
resolve: {
extensions: [".ts", ".mjs", ".js", ".json"],
mainFields: ["main"],
modules: [paths.rootNodeModules, ...moduleDirs],
plugins: [
new LinkedPackageResolvePlugin(paths.rootNodeModules, externalPkgs),
new ModuleScopePlugin__default["default"](
[paths.targetSrc, paths.targetDev],
[paths.targetPackageJson]
)
]
},
module: {
rules: loaders
},
output: {
path: paths.targetDist,
filename: isDev ? "[name].js" : "[name].[hash:8].js",
chunkFilename: isDev ? "[name].chunk.js" : "[name].[chunkhash:8].chunk.js",
...isDev ? {
devtoolModuleFilenameTemplate: (info) => `file:///${path.resolve(info.absoluteResourcePath).replace(
/\\/g,
"/"
)}`
} : {}
},
plugins: [
new runScriptWebpackPlugin.RunScriptWebpackPlugin({
name: "main.js",
nodeArgs: runScriptNodeArgs.length > 0 ? runScriptNodeArgs : void 0,
args: process.argv.slice(3)
// drop `node backstage-cli backend:dev`
}),
new webpack__default["default"].HotModuleReplacementPlugin(),
...checksEnabled ? [
new ForkTsCheckerWebpackPlugin__default["default"]({
typescript: { configFile: paths.targetTsConfig }
}),
new ESLintPlugin__default["default"]({
files: ["**/*.(ts|tsx|mts|cts|js|jsx|mjs|cjs)"]
})
] : []
]
};
}
function nodeExternalsWithResolve(options) {
let currentContext;
const externals = nodeExternals__default["default"]({
...options,
importType(request) {
const resolved = require.resolve(request, {
paths: [currentContext]
});
return `commonjs ${resolved}`;
}
});
return ({ context, request }, callback) => {
currentContext = context;
return externals(context, request, callback);
};
}
function resolveBundlingPaths(options) {
const { entry, targetDir = index.paths.targetDir } = options;
const resolveTargetModule = (pathString) => {
for (const ext of ["mjs", "js", "ts", "tsx", "jsx"]) {
const filePath = path.resolve(targetDir, `${pathString}.${ext}`);
if (fs__default["default"].pathExistsSync(filePath)) {
return filePath;
}
}
return path.resolve(targetDir, `${pathString}.js`);
};
let targetPublic = void 0;
let targetHtml = path.resolve(targetDir, "public/index.html");
if (fs__default["default"].pathExistsSync(targetHtml)) {
targetPublic = path.resolve(targetDir, "public");
} else {
targetHtml = path.resolve(targetDir, `${entry}.html`);
if (!fs__default["default"].pathExistsSync(targetHtml)) {
targetHtml = index.paths.resolveOwn("templates/serve_index.html");
}
}
const targetRunFile = path.resolve(targetDir, "src/run.ts");
const runFileExists = fs__default["default"].pathExistsSync(targetRunFile);
return {
targetHtml,
targetPublic,
targetPath: path.resolve(targetDir, "."),
targetRunFile: runFileExists ? targetRunFile : void 0,
targetDist: path.resolve(targetDir, "dist"),
targetAssets: path.resolve(targetDir, "assets"),
targetSrc: path.resolve(targetDir, "src"),
targetDev: path.resolve(targetDir, "dev"),
targetEntry: resolveTargetModule(entry),
targetTsConfig: index.paths.resolveTargetRoot("tsconfig.json"),
targetPackageJson: path.resolve(targetDir, "package.json"),
rootNodeModules: index.paths.resolveTargetRoot("node_modules"),
root: index.paths.targetRoot
};
}
const DETECTED_MODULES_MODULE_NAME = "__backstage-autodetected-plugins__";
function readPackageDetectionConfig(config$1) {
const packages = config$1.getOptional("app.experimental.packages");
if (packages === void 0 || packages === null) {
return void 0;
}
if (typeof packages === "string") {
if (packages !== "all") {
throw new Error(
`Invalid app.experimental.packages mode, got '${packages}', expected 'all'`
);
}
return {};
}
if (typeof packages !== "object" || Array.isArray(packages)) {
throw new Error(
"Invalid config at 'app.experimental.packages', expected object"
);
}
const packagesConfig = new config.ConfigReader(
packages,
"app.experimental.packages"
);
return {
include: packagesConfig.getOptionalStringArray("include"),
exclude: packagesConfig.getOptionalStringArray("exclude")
};
}
async function detectPackages(targetPath, { include, exclude }) {
var _a;
const pkg = await fs__default["default"].readJson(
path.resolve(targetPath, "package.json")
);
return Object.keys((_a = pkg.dependencies) != null ? _a : {}).flatMap((depName) => {
var _a2, _b;
if (exclude == null ? void 0 : exclude.includes(depName)) {
return [];
}
if (include && !include.includes(depName)) {
return [];
}
try {
const depPackageJson = require(require.resolve(
`${depName}/package.json`,
{ paths: [targetPath] }
));
if (["frontend-plugin", "frontend-plugin-module"].includes(
(_b = (_a2 = depPackageJson.backstage) == null ? void 0 : _a2.role) != null ? _b : ""
)) {
const exp = depPackageJson.exports;
if (exp && typeof exp === "object" && "./alpha" in exp) {
return [
{ name: depName, import: depName },
{ name: depName, export: "./alpha", import: `${depName}/alpha` }
];
}
return [{ name: depName, import: depName }];
}
} catch {
}
return [];
});
}
async function writeDetectedPackagesModule(pkgs) {
const requirePackageScript = pkgs == null ? void 0 : pkgs.map(
(pkg) => `{ name: ${JSON.stringify(pkg.name)}, export: ${JSON.stringify(
pkg.export
)}, default: require('${pkg.import}').default }`
).join(",");
await fs__default["default"].writeFile(
path.join(
index.paths.targetRoot,
"node_modules",
`${DETECTED_MODULES_MODULE_NAME}.js`
),
`window['__@backstage/discovered__'] = { modules: [${requirePackageScript}] };`
);
}
async function createDetectedModulesEntryPoint(options) {
const { config, watch, targetPath } = options;
const detectionConfig = readPackageDetectionConfig(config);
if (!detectionConfig) {
return [];
}
if (watch) {
const watcher = chokidar__default["default"].watch(path.resolve(targetPath, "package.json"));
watcher.on("change", async () => {
await writeDetectedPackagesModule(
await detectPackages(targetPath, detectionConfig)
);
watch();
});
}
await writeDetectedPackagesModule(
await detectPackages(targetPath, detectionConfig)
);
return [DETECTED_MODULES_MODULE_NAME];
}
exports.createBackendConfig = createBackendConfig;
exports.createConfig = createConfig;
exports.createDetectedModulesEntryPoint = createDetectedModulesEntryPoint;
exports.hasReactDomClient = hasReactDomClient;
exports.resolveBaseUrl = resolveBaseUrl;
exports.resolveBundlingPaths = resolveBundlingPaths;
//# sourceMappingURL=packageDetection-a9880107.cjs.js.map