diff --git a/src/commands/export-dynamic-plugin/backend.ts b/src/commands/export-dynamic-plugin/backend.ts index 5a0e5f0..fed6320 100644 --- a/src/commands/export-dynamic-plugin/backend.ts +++ b/src/commands/export-dynamic-plugin/backend.ts @@ -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'; @@ -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 { const targetRelativePath = 'dist-dynamic'; @@ -209,7 +217,7 @@ throw new Error( isYarnV1: yarnVersion.startsWith('1.'), monoRepoPackages, sharedPackages: sharedPackagesRules, - overridding: { + overriding: { private: true, version: `${embedded.version}+embedded`, }, @@ -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. @@ -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) { @@ -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[], @@ -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 & { - bundleDependencies?: boolean; - }) - | undefined; - additionalOverrides?: { [key: string]: any } | undefined; - additionalResolutions?: { [key: string]: any } | undefined; - after?: ((pkg: BackstagePackageJson) => void) | undefined; -}): (dynamicPkgPath: string) => Promise { - return async (dynamicPkgPath: string): Promise => { - 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`); @@ -941,10 +711,3 @@ function validatePluginEntryPoints(target: string): string { return ''; } - -function embeddedPackageRelativePath(p: ResolvedEmbedded): string { - return path.join( - 'embedded', - p.packageName.replace(/^@/, '').replace(/\//, '-'), - ); -} diff --git a/src/commands/export-dynamic-plugin/command.ts b/src/commands/export-dynamic-plugin/command.ts index 74a9103..6732157 100644 --- a/src/commands/export-dynamic-plugin/command.ts +++ b/src/commands/export-dynamic-plugin/command.ts @@ -38,7 +38,6 @@ export async function command(opts: OptionValues): Promise { } let targetPath: string; - const roleInfo = PackageRoles.getRoleInfo(role); let configSchemaPaths: string[]; if (role === 'backend-plugin' || role === 'backend-plugin-module') { targetPath = await backend(opts); @@ -47,7 +46,7 @@ export async function command(opts: OptionValues): Promise { 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( @@ -77,6 +76,7 @@ export async function command(opts: OptionValues): Promise { await checkBackstageSupportedVersions(targetPath); + const roleInfo = PackageRoles.getRoleInfo(role); await applyDevOptions(opts, rawPkg.name, roleInfo, targetPath); } diff --git a/src/commands/export-dynamic-plugin/common-utils.ts b/src/commands/export-dynamic-plugin/common-utils.ts new file mode 100644 index 0000000..38ab9b0 --- /dev/null +++ b/src/commands/export-dynamic-plugin/common-utils.ts @@ -0,0 +1,246 @@ +import chalk from 'chalk'; +import fs from 'fs-extra'; + +import path from 'node:path'; +import { BackstagePackageJson } from '@backstage/cli-node'; + +import { Packages } from '@manypkg/get-packages'; +import * as semver from 'semver'; +import { ResolvedEmbedded, SharedPackagesRules } from './types'; +import { Task } from '../../lib/tasks'; + +export 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; + sharedPackages?: SharedPackagesRules; + overriding?: Partial & { + bundleDependencies?: boolean; + }; + additionalOverrides?: { [key: string]: any }; + additionalResolutions?: { [key: string]: any }; + after?: (pkg: BackstagePackageJson) => void; +}): (dynamicPkgPath: string) => Promise { + return async (dynamicPkgPath: string): Promise => { + const dynamicPkgContent = await fs.readFile(dynamicPkgPath, 'utf8'); + const pkgToCustomize = JSON.parse( + dynamicPkgContent, + ) as BackstagePackageJson; + + for (const field in options.overriding || {}) { + if (!Object.hasOwn(options.overriding || {}, field)) { + continue; + } + (pkgToCustomize as any)[field] = (options.overriding as any)[field]; + } + + pkgToCustomize.files = pkgToCustomize.files?.filter( + f => !f.startsWith('dist-dynamic/'), + ); + + if (pkgToCustomize.dependencies) { + for (const dep in pkgToCustomize.dependencies) { + if (!Object.hasOwn(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, + }); + }; +} + +export async function locateAndCopyYarnLock({ + targetDir, + targetRoot, + yarnLock, +}: { + targetDir: string; + targetRoot: string; + yarnLock: string; +}) { + // 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(targetDir, 'yarn.lock'))) { + staticPluginYarnLock = path.join(targetDir, 'yarn.lock'); + } else if (await fs.pathExists(path.join(targetRoot, 'yarn.lock'))) { + staticPluginYarnLock = path.join(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( + targetRoot, + )})`, + ); + } + await fs.copyFile(staticPluginYarnLock, yarnLock); +} + +export function isPackageShared( + pkgName: string, + rules: SharedPackagesRules | undefined, +) { + const 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; +} + +export function embeddedPackageRelativePath(p: ResolvedEmbedded): string { + return path.join( + 'embedded', + p.packageName.replace(/^@/, '').replace(/\//, '-'), + ); +} diff --git a/src/commands/export-dynamic-plugin/frontend.ts b/src/commands/export-dynamic-plugin/frontend.ts index f17efc0..427b0f8 100644 --- a/src/commands/export-dynamic-plugin/frontend.ts +++ b/src/commands/export-dynamic-plugin/frontend.ts @@ -5,7 +5,7 @@ * 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 + * 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, @@ -14,26 +14,26 @@ * limitations under the License. */ -import { PackageRoleInfo } from '@backstage/cli-node'; import { buildFrontend } from '@backstage/cli/dist/modules/build/lib/buildFrontend.cjs.js'; - import { getPackages } from '@manypkg/get-packages'; import chalk from 'chalk'; import { OptionValues } from 'commander'; import fs from 'fs-extra'; - import path from 'path'; +import { execSync } from 'node:child_process'; import { buildScalprumPlugin } from '../../lib/builder/buildScalprumPlugin'; import { productionPack } from '../../lib/packager/productionPack'; import { paths } from '../../lib/paths'; import { Task } from '../../lib/tasks'; -import { customizeForDynamicUse } from './backend'; +import { customizeForDynamicUse, locateAndCopyYarnLock } from './common-utils'; -export async function frontend( - _: PackageRoleInfo, - opts: OptionValues, -): Promise { +/** + * The main entrypoint for exporting frontend Backstage plugins + * @param opts + * @returns + */ +export async function frontend(opts: OptionValues): Promise { const { name, version, @@ -47,29 +47,16 @@ export async function frontend( ); } - if (opts.generateModuleFederationAssets) { - if (opts.clean) { - await fs.remove(path.join(paths.targetDir, 'dist')); - } + // 1. Generate Module Federation Assets + await generateModuleFederationAssets(opts); - Task.log( - `Generating standard module federation assets in ${chalk.cyan( - path.join(paths.targetDir, 'dist'), - )}`, - ); - await buildFrontend({ - targetDir: paths.targetDir, - configPaths: [], - writeStats: false, - isModuleFederationRemote: true, - }); - } + // 2. Prepare Target Directory + const targetRelativePath = 'dist-dynamic'; + const target = path.resolve(paths.targetDir, targetRelativePath); - const distDynamicRelativePath = 'dist-dynamic'; - const target = path.resolve(paths.targetDir, distDynamicRelativePath); Task.log( `Packing main package to ${chalk.cyan( - path.join(distDynamicRelativePath, 'package.json'), + path.join(targetRelativePath, 'package.json'), )}`, ); @@ -78,23 +65,20 @@ export async function frontend( } await fs.mkdirs(target); - await fs.writeFile( - path.join(target, '.gitignore'), - ` -* -`, - ); + await fs.writeFile(path.join(target, '.gitignore'), `\n*\n`); await productionPack({ packageDir: paths.targetDir, targetDir: target, }); + // 3. Customize Package.json Task.log( `Customizing main package in ${chalk.cyan( - path.join(distDynamicRelativePath, 'package.json'), + path.join(targetRelativePath, 'package.json'), )} for dynamic loading`, ); + if ( files && Array.isArray(files) && @@ -103,75 +87,164 @@ export async function frontend( ) { files.push('dist-scalprum'); } + const monoRepoPackages = await getPackages(paths.targetDir); await customizeForDynamicUse({ embedded: [], isYarnV1: false, monoRepoPackages, - overridding: { + overriding: { name: `${name}-dynamic`, - // We remove scripts, because they do not make sense for this derived package. - // They even bring errors, especially the pre-pack and post-pack ones: - // we want to be able to use npm pack on this derived package to distribute it as a dynamic plugin, - // and obviously this should not trigger the backstage pre-pack or post-pack actions - // which are related to the packaging of the original static package. - scripts: {}, + scripts: {}, // Scripts removed to avoid npm pack triggers files, }, })(path.resolve(target, 'package.json')); - if (opts.generateScalprumAssets) { - const resolvedScalprumDistPath = path.join(target, 'dist-scalprum'); + // 4. Generate Scalprum Assets + await generateScalprumAssets(opts, target, name, version, scalprumInline); + + // 5. Handle Yarn Install / Lockfile + await handlePackageInstall(opts, target); + + return target; +} + +async function generateModuleFederationAssets(opts: OptionValues) { + if (!opts.generateModuleFederationAssets) return; + + if (opts.clean) { + await fs.remove(path.join(paths.targetDir, 'dist')); + } + + Task.log( + `Generating standard module federation assets in ${chalk.cyan( + path.join(paths.targetDir, 'dist'), + )}`, + ); + await buildFrontend({ + targetDir: paths.targetDir, + configPaths: [], + writeStats: false, + isModuleFederationRemote: true, + }); +} + +async function resolveScalprumConfig( + opts: OptionValues, + scalprumInline: any, + name: string, +) { + if (opts.scalprumConfig) { + const scalprumConfigFile = paths.resolveTarget(opts.scalprumConfig); Task.log( - `Generating dynamic frontend plugin assets in ${chalk.cyan( - resolvedScalprumDistPath, - )}`, + `Using external scalprum config file: ${chalk.cyan(scalprumConfigFile)}`, ); + return fs.readJson(scalprumConfigFile); + } - let scalprum: any = undefined; - if (opts.scalprumConfig) { - const scalprumConfigFile = paths.resolveTarget(opts.scalprumConfig); - Task.log( - `Using external scalprum config file: ${chalk.cyan(scalprumConfigFile)}`, - ); - scalprum = await fs.readJson(scalprumConfigFile); - } else if (scalprumInline) { - Task.log(`Using scalprum config inlined in the 'package.json'`); - scalprum = scalprumInline; - } else { - let scalprumName; - if (name.includes('/')) { - const fragments = name.split('/'); - scalprumName = `${fragments[0].replace('@', '')}.${fragments[1]}`; - } else { - scalprumName = name; - } - scalprum = { - name: scalprumName, - exposedModules: { - PluginRoot: './src/index.ts', - }, - }; - Task.log(`No scalprum config. Using default dynamic UI configuration:`); - Task.log(chalk.cyan(JSON.stringify(scalprum, null, 2))); - Task.log( - `If you wish to change the defaults, add "scalprum" configuration to plugin "package.json" file, or use the '--scalprum-config' option to specify an external config.`, - ); - } - - await fs.remove(resolvedScalprumDistPath); - - await buildScalprumPlugin({ - writeStats: false, - configPaths: [], - targetDir: paths.targetDir, - pluginMetadata: { - ...scalprum, - version, - }, + if (scalprumInline) { + Task.log(`Using scalprum config inlined in the 'package.json'`); + return scalprumInline; + } + + // Default configuration generation + let scalprumName; + if (name.includes('/')) { + const fragments = name.split('/'); + scalprumName = `${fragments[0].replace('@', '')}.${fragments[1]}`; + } else { + scalprumName = name; + } + + const defaultScalprum = { + name: scalprumName, + exposedModules: { + PluginRoot: './src/index.ts', + }, + }; + + Task.log(`No scalprum config. Using default dynamic UI configuration:`); + Task.log(chalk.cyan(JSON.stringify(defaultScalprum, null, 2))); + Task.log( + `If you wish to change the defaults, add "scalprum" configuration to plugin "package.json" file, or use the '--scalprum-config' option to specify an external config.`, + ); + return defaultScalprum; +} + +async function generateScalprumAssets( + opts: OptionValues, + target: string, + name: string, + version: string, + scalprumInline: any, +) { + if (!opts.generateScalprumAssets) return; + + const resolvedScalprumDistPath = path.join(target, 'dist-scalprum'); + Task.log( + `Generating dynamic frontend plugin assets in ${chalk.cyan( resolvedScalprumDistPath, + )}`, + ); + + const scalprum = await resolveScalprumConfig(opts, scalprumInline, name); + + await fs.remove(resolvedScalprumDistPath); + + await buildScalprumPlugin({ + writeStats: false, + configPaths: [], + targetDir: paths.targetDir, + pluginMetadata: { + ...scalprum, + version, + }, + resolvedScalprumDistPath, + }); +} + +async function handlePackageInstall(opts: OptionValues, target: string) { + const yarn = 'yarn'; + const yarnVersion = execSync(`${yarn} --version`).toString().trim(); // NOSONAR + const yarnLock = path.resolve(target, 'yarn.lock'); + const yarnLockExists = await fs.pathExists(yarnLock); + + if (!yarnLockExists) { + await locateAndCopyYarnLock({ + targetDir: paths.targetDir, + targetRoot: paths.targetRoot, + yarnLock, }); } - return target; + 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`, + ), + ); + return; + } + + Task.log( + `${yarnLockExists ? 'Verifying' : 'Creating'} filtered yarn.lock file for the exported package`, + ); + + const logFile = 'yarn-install.log'; + const redirect = `> ${logFile}`; + const yarnInstall = yarnVersion.startsWith('1.') + ? `${yarn} install --production${ + yarnLockExists ? ' --frozen-lockfile' : '' + } ${redirect}` + : `${yarn} install${yarnLockExists ? ' --immutable' : ' --no-immutable'} ${redirect}`; + + await Task.forCommand(yarnInstall, { cwd: target, optional: false }); + await fs.remove(paths.resolveTarget('dist-dynamic', '.yarn')); + await fs.remove(paths.resolveTarget('dist-dynamic', logFile)); } diff --git a/src/commands/export-dynamic-plugin/types.ts b/src/commands/export-dynamic-plugin/types.ts new file mode 100644 index 0000000..3df1300 --- /dev/null +++ b/src/commands/export-dynamic-plugin/types.ts @@ -0,0 +1,12 @@ +export type SharedPackagesRules = { + include: (string | RegExp)[]; + exclude: (string | RegExp)[]; +}; + +export type ResolvedEmbedded = { + packageName: string; + version: string; + dir: string; + parentPackageName: string; + alreadyPacked: boolean; +};