Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 28 additions & 265 deletions src/commands/export-dynamic-plugin/backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { OptionValues } from 'commander';
import * as fs from 'fs-extra';
import * as semver from 'semver';

import { execSync } from 'child_process';
import { execSync } from 'node:child_process';
import { createRequire } from 'node:module';
import * as path from 'path';

Expand All @@ -37,6 +37,14 @@ import {
gatherNativeModules,
isValidPluginModule,
} from './backend-utils';
import {
checkWorkspacePackageVersion,
customizeForDynamicUse,
embeddedPackageRelativePath,
isPackageShared,
locateAndCopyYarnLock,
} from './common-utils';
import { ResolvedEmbedded, SharedPackagesRules } from './types';

export async function backend(opts: OptionValues): Promise<string> {
const targetRelativePath = 'dist-dynamic';
Expand Down Expand Up @@ -209,7 +217,7 @@ throw new Error(
isYarnV1: yarnVersion.startsWith('1.'),
monoRepoPackages,
sharedPackages: sharedPackagesRules,
overridding: {
overriding: {
private: true,
version: `${embedded.version}+embedded`,
},
Expand Down Expand Up @@ -276,7 +284,7 @@ throw new Error(
isYarnV1: yarnVersion.startsWith('1.'),
monoRepoPackages,
sharedPackages: sharedPackagesRules,
overridding: {
overriding: {
name: derivedPackageName,
bundleDependencies: true,
// We remove scripts, because they do not make sense for this derived package.
Expand Down Expand Up @@ -317,40 +325,11 @@ throw new Error(
const yarnLockExists = await fs.pathExists(yarnLock);

if (!yarnLockExists) {
// Search the yarn.lock of the static plugin, possibly at the root of the monorepo.

let staticPluginYarnLock: string | undefined;
if (await fs.pathExists(path.join(paths.targetDir, 'yarn.lock'))) {
staticPluginYarnLock = path.join(paths.targetDir, 'yarn.lock');
} else if (await fs.pathExists(path.join(paths.targetRoot, 'yarn.lock'))) {
staticPluginYarnLock = path.join(paths.targetRoot, 'yarn.lock');
}

if (!staticPluginYarnLock) {
throw new Error(
`Could not find the static plugin ${chalk.cyan(
'yarn.lock',
)} file in either the local folder or the monorepo root (${chalk.cyan(
paths.targetRoot,
)})`,
);
}

await fs.copyFile(staticPluginYarnLock, yarnLock);

if (!opts.install) {
Task.log(
chalk.yellow(
`Last export step (${chalk.cyan(
'yarn install',
)} has been disabled: the dynamic plugin package ${chalk.cyan(
'yarn.lock',
)} file will be inconsistent until ${chalk.cyan(
'yarn install',
)} is run manually`,
),
);
}
await locateAndCopyYarnLock({
targetDir: paths.targetDir,
targetRoot: paths.targetRoot,
yarnLock,
});
}

if (opts.install) {
Expand Down Expand Up @@ -471,18 +450,22 @@ throw new Error(
}
// everything is fine, remove the yarn install log
await fs.remove(paths.resolveTarget(targetRelativePath, logFile));
} else {
Task.log(
chalk.yellow(
`Last export step (${chalk.cyan(
'yarn install',
)} has been disabled: the dynamic plugin package ${chalk.cyan(
'yarn.lock',
)} file will be inconsistent until ${chalk.cyan(
'yarn install',
)} is run manually`,
),
);
}
return target;
}

type ResolvedEmbedded = {
packageName: string;
version: string;
dir: string;
parentPackageName: string;
alreadyPacked: boolean;
};

async function searchEmbedded(
pkg: BackstagePackageJson,
packagesToEmbed: string[],
Expand Down Expand Up @@ -638,219 +621,6 @@ async function searchEmbedded(
return resolved;
}

function checkWorkspacePackageVersion(
requiredVersionSpec: string,
pkg: { version: string; dir: string },
): boolean {
const versionDetail = requiredVersionSpec.replace(/^workspace:/, '');

return (
pkg.dir === versionDetail ||
versionDetail === '*' ||
versionDetail === '~' ||
versionDetail === '^' ||
semver.satisfies(pkg.version, versionDetail)
);
}

export function customizeForDynamicUse(options: {
embedded: ResolvedEmbedded[];
isYarnV1: boolean;
monoRepoPackages: Packages | undefined;
sharedPackages?: SharedPackagesRules | undefined;
overridding?:
| (Partial<BackstagePackageJson> & {
bundleDependencies?: boolean;
})
| undefined;
additionalOverrides?: { [key: string]: any } | undefined;
additionalResolutions?: { [key: string]: any } | undefined;
after?: ((pkg: BackstagePackageJson) => void) | undefined;
}): (dynamicPkgPath: string) => Promise<void> {
return async (dynamicPkgPath: string): Promise<void> => {
const dynamicPkgContent = await fs.readFile(dynamicPkgPath, 'utf8');
const pkgToCustomize = JSON.parse(
dynamicPkgContent,
) as BackstagePackageJson;

for (const field in options.overridding || {}) {
if (!Object.prototype.hasOwnProperty.call(options.overridding, field)) {
continue;
}
(pkgToCustomize as any)[field] = (options.overridding as any)[field];
}

pkgToCustomize.files = pkgToCustomize.files?.filter(
f => !f.startsWith('dist-dynamic/'),
);

if (pkgToCustomize.dependencies) {
for (const dep in pkgToCustomize.dependencies) {
if (
!Object.prototype.hasOwnProperty.call(
pkgToCustomize.dependencies,
dep,
)
) {
continue;
}

const dependencyVersionSpec = pkgToCustomize.dependencies[dep];
if (dependencyVersionSpec.startsWith('workspace:')) {
let resolvedVersion: string | undefined;
const rangeSpecifier = dependencyVersionSpec.replace(
/^workspace:/,
'',
);
const embeddedDep = options.embedded.find(
e =>
e.packageName === dep &&
checkWorkspacePackageVersion(dependencyVersionSpec, e),
);
if (embeddedDep) {
resolvedVersion = embeddedDep.version;
} else if (options.monoRepoPackages) {
const relatedMonoRepoPackages =
options.monoRepoPackages.packages.filter(
p => p.packageJson.name === dep,
);
if (relatedMonoRepoPackages.length > 1) {
throw new Error(
`Two packages named ${chalk.cyan(
dep,
)} exist in the monorepo structure: this is not supported.`,
);
}
if (
relatedMonoRepoPackages.length === 1 &&
checkWorkspacePackageVersion(dependencyVersionSpec, {
dir: relatedMonoRepoPackages[0].dir,
version: relatedMonoRepoPackages[0].packageJson.version,
})
) {
resolvedVersion =
rangeSpecifier === '^' || rangeSpecifier === '~'
? rangeSpecifier +
relatedMonoRepoPackages[0].packageJson.version
: relatedMonoRepoPackages[0].packageJson.version;
}
}

if (!resolvedVersion) {
throw new Error(
`Workspace dependency ${chalk.cyan(dep)} of package ${chalk.cyan(
pkgToCustomize.name,
)} doesn't exist in the monorepo structure: maybe you should embed it ?`,
);
}

pkgToCustomize.dependencies[dep] = resolvedVersion;
}

if (isPackageShared(dep, options.sharedPackages)) {
Task.log(` moving ${chalk.cyan(dep)} to peerDependencies`);

pkgToCustomize.peerDependencies ||= {};
pkgToCustomize.peerDependencies[dep] =
pkgToCustomize.dependencies[dep];
delete pkgToCustomize.dependencies[dep];

continue;
}

// If yarn v1, then detect if the current dep is an embedded one,
// and if it is the case replace the version by the file protocol
// (like what we do for the resolutions).
if (options.isYarnV1) {
const embeddedDep = options.embedded.find(
e =>
e.packageName === dep &&
checkWorkspacePackageVersion(dependencyVersionSpec, e),
);
if (embeddedDep) {
pkgToCustomize.dependencies[dep] =
`file:./${embeddedPackageRelativePath(embeddedDep)}`;
}
}
}
}

// We remove devDependencies here since we want the dynamic plugin derived package
// to get only production dependencies, and no transitive dependencies, in both
// the node_modules sub-folder and yarn.lock file in `dist-dynamic`.
//
// And it happens that `yarn install --production` (yarn 1) doesn't completely
// remove devDependencies as needed.
//
// See https://github.com/yarnpkg/yarn/issues/6373#issuecomment-760068356
pkgToCustomize.devDependencies = {};

// additionalOverrides and additionalResolutions will override the
// current package.json entries for "overrides" and "resolutions"
// respectively
const overrides = (pkgToCustomize as any).overrides || {};
(pkgToCustomize as any).overrides = {
// The following lines are a workaround for the fact that the @aws-sdk/util-utf8-browser package
// is not compatible with the NPM 9+, so that `npm pack` would not grab the Javascript files.
// This package has been deprecated in favor of @smithy/util-utf8.
//
// See https://github.com/aws/aws-sdk-js-v3/issues/5305.
'@aws-sdk/util-utf8-browser': {
'@smithy/util-utf8': '^2.0.0',
},
...overrides,
...(options.additionalOverrides || {}),
};
const resolutions = (pkgToCustomize as any).resolutions || {};
(pkgToCustomize as any).resolutions = {
// The following lines are a workaround for the fact that the @aws-sdk/util-utf8-browser package
// is not compatible with the NPM 9+, so that `npm pack` would not grab the Javascript files.
// This package has been deprecated in favor of @smithy/util-utf8.
//
// See https://github.com/aws/aws-sdk-js-v3/issues/5305.
'@aws-sdk/util-utf8-browser': 'npm:@smithy/util-utf8@~2',
...resolutions,
...(options.additionalResolutions || {}),
};

if (options.after) {
options.after(pkgToCustomize);
}

await fs.writeJson(dynamicPkgPath, pkgToCustomize, {
encoding: 'utf8',
spaces: 2,
});
};
}

type SharedPackagesRules = {
include: (string | RegExp)[];
exclude: (string | RegExp)[];
};

function isPackageShared(
pkgName: string,
rules: SharedPackagesRules | undefined,
) {
function test(str: string, expr: string | RegExp): boolean {
if (typeof expr === 'string') {
return str === expr;
}
return expr.test(str);
}

if ((rules?.exclude || []).some(dontMove => test(pkgName, dontMove))) {
return false;
}

if ((rules?.include || []).some(move => test(pkgName, move))) {
return true;
}

return false;
}

function validatePluginEntryPoints(target: string): string {
const dynamicPluginRequire = createRequire(`${target}/package.json`);

Expand Down Expand Up @@ -941,10 +711,3 @@ function validatePluginEntryPoints(target: string): string {

return '';
}

function embeddedPackageRelativePath(p: ResolvedEmbedded): string {
return path.join(
'embedded',
p.packageName.replace(/^@/, '').replace(/\//, '-'),
);
}
4 changes: 2 additions & 2 deletions src/commands/export-dynamic-plugin/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export async function command(opts: OptionValues): Promise<void> {
}

let targetPath: string;
const roleInfo = PackageRoles.getRoleInfo(role);
let configSchemaPaths: string[];
if (role === 'backend-plugin' || role === 'backend-plugin-module') {
targetPath = await backend(opts);
Expand All @@ -47,7 +46,7 @@ export async function command(opts: OptionValues): Promise<void> {
path.join(targetPath, 'dist/.config-schema.json'),
];
} else if (role === 'frontend-plugin' || role === 'frontend-plugin-module') {
targetPath = await frontend(roleInfo, opts);
targetPath = await frontend(opts);
configSchemaPaths = [];
if (fs.existsSync(path.join(targetPath, 'dist-scalprum'))) {
configSchemaPaths.push(
Expand Down Expand Up @@ -77,6 +76,7 @@ export async function command(opts: OptionValues): Promise<void> {

await checkBackstageSupportedVersions(targetPath);

const roleInfo = PackageRoles.getRoleInfo(role);
await applyDevOptions(opts, rawPkg.name, roleInfo, targetPath);
}

Expand Down
Loading