UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

606 lines (604 loc) 22 kB
import { isPlainObject, resolveExtendsRef } from "../merge.mjs"; import { saveBosConfig } from "../utils/save-config.mjs"; import { fetchParentConfig, resolveSourceDir, runBunInstallForUpgrade, runTypesGen } from "./init.mjs"; import { readInstalledFrameworkVersion } from "./framework-version.mjs"; import { syncTemplate } from "./sync.mjs"; import { timePhase } from "./timing.mjs"; import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import process from "node:process"; import * as p from "@clack/prompts"; import { glob } from "glob"; //#region src/cli/upgrade.ts const FRAMEWORK_PACKAGES = ["everything-dev", "every-plugin"]; const CATALOG_TOOL_PACKAGES = [ "@rspack/core", "@rspack/cli", "@rsbuild/core", "@rsbuild/plugin-react", "@module-federation/enhanced", "@module-federation/node", "@module-federation/rsbuild-plugin", "@module-federation/runtime-core", "@module-federation/sdk", "@module-federation/dts-plugin" ]; const PINNED_CATALOG_TOOL_VERSIONS = { "@rspack/core": "1.7.11", "@rspack/cli": "1.7.11", "@rsbuild/core": "1.7.5", "@rsbuild/plugin-react": "1.4.6" }; const LEGACY_UI_IMPORT_REWRITES = [ ["from \"@/auth\"", "from \"@/app\""], ["from '@/auth'", "from '@/app'"], ["from \"@/lib/use-api-client\"", "from \"@/app\""], ["from '@/lib/use-api-client'", "from '@/app'"], ["from \"@/lib/api-client\"", "from \"@/app\""], ["from '@/lib/api-client'", "from '@/app'"] ]; const OBSOLETE_FILES = [ "ui/src/auth.ts", "ui/src/auth-types.gen.ts", "ui/src/lib/api-client.ts", "ui/src/lib/use-api-client.ts", "ui/src/api-contract.ts", "ui/src/api-contract.gen.ts", "ui/src/lib/auth-client.ts", "ui/src/lib/session.ts", "ui/scripts/generate-metadata.ts", ".github/dependabot.yml", ".github/templates/dependabot.yml", "packages/everything-dev/cli.js", ".templatekeep", ".templatesync-exclude" ]; function extractVersion(value) { if (!value) return null; return value.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/)?.[0] ?? null; } async function readExtendedRootCatalog(projectDir) { const configPath = join(projectDir, "bos.config.json"); if (!existsSync(configPath)) return {}; const localConfig = JSON.parse(readFileSync(configPath, "utf-8")); let extendsRef; if (typeof localConfig.extends === "string") extendsRef = localConfig.extends; else if (isPlainObject(localConfig.extends)) extendsRef = resolveExtendsRef(localConfig.extends, "production"); const parsed = extendsRef ? parseBosRef(extendsRef) : null; if (!parsed) return {}; const { sourceDir, cleanup } = await resolveSourceDir({ extendsAccount: parsed.account, extendsGateway: parsed.gateway }); try { const sourcePkgPath = join(sourceDir, "package.json"); if (!existsSync(sourcePkgPath)) return {}; return { ...JSON.parse(readFileSync(sourcePkgPath, "utf-8")).workspaces?.catalog ?? {} }; } finally { await cleanup(); } } function getExtendsRef(config) { if (typeof config.extends === "string") return config.extends; if (config.extends && typeof config.extends === "object") return resolveExtendsRef(config.extends, "production"); } function parseBosRef(ref) { const match = ref.match(/^bos:\/\/([^/]+)\/(.+)$/); if (!match?.[1] || !match[2]) return null; return { account: match[1], gateway: match[2] }; } function parseTargetedRef(ref) { const hashIndex = ref.indexOf("#"); if (hashIndex === -1) return { configRef: ref }; return { configRef: ref.slice(0, hashIndex), targetPath: ref.slice(hashIndex + 1) || void 0 }; } function ensureTargetedRef(ref, targetPath) { const parsed = parseTargetedRef(ref); if (parsed.targetPath) return ref; return `${parsed.configRef}#${targetPath}`; } function rewriteExtendsTarget(entry, targetPath) { if (!entry?.extends) return false; if (typeof entry.extends === "string") { const next = ensureTargetedRef(entry.extends, targetPath); if (next === entry.extends) return false; entry.extends = next; return true; } if (typeof entry.extends === "object") { let changed = false; for (const [key, value] of Object.entries(entry.extends)) { if (typeof value !== "string") continue; const next = ensureTargetedRef(value, targetPath); if (next !== value) { entry.extends[key] = next; changed = true; } } return changed; } return false; } function migrateRootConfigTargets(config) { let changed = false; const app = config.app && typeof config.app === "object" ? config.app : void 0; if (app?.api && typeof app.api === "object") changed = rewriteExtendsTarget(app.api, "app.api") || changed; if (app?.auth && typeof app.auth === "object") changed = rewriteExtendsTarget(app.auth, "app.auth") || changed; if (config.plugins && typeof config.plugins === "object") for (const [pluginKey, pluginValue] of Object.entries(config.plugins)) { if (typeof pluginValue === "string") { const next = ensureTargetedRef(pluginValue, `plugins.${pluginKey}`); if (next !== pluginValue) { config.plugins[pluginKey] = next; changed = true; } continue; } if (!pluginValue || typeof pluginValue !== "object") continue; changed = rewriteExtendsTarget(pluginValue, `plugins.${pluginKey}`) || changed; } return changed; } function migratePluginProviderConfig(config, pluginKey) { let changed = false; if (!config.plugins || typeof config.plugins !== "object") return false; const entry = config.plugins[pluginKey]; if (!entry || typeof entry !== "object") return false; const pluginEntry = entry; if ("name" in pluginEntry) { delete pluginEntry.name; changed = true; } if (typeof pluginEntry.development === "string" && pluginEntry.development.startsWith("local:")) { if ("extends" in pluginEntry) { delete pluginEntry.extends; changed = true; } } changed = rewriteExtendsTarget(pluginEntry, `plugins.${pluginKey}`) || changed; return changed; } function mergePluginConfigIntoRoot(rootConfig, pluginKey, pluginConfig) { let changed = false; if (!rootConfig.plugins || typeof rootConfig.plugins !== "object") { rootConfig.plugins = {}; changed = true; } const plugins = rootConfig.plugins; if (!plugins[pluginKey] || typeof plugins[pluginKey] !== "object") { plugins[pluginKey] = {}; changed = true; } const entry = plugins[pluginKey]; const pluginData = extractPluginEntry(pluginConfig, pluginKey); const apiData = getApiEntry(pluginConfig); if (pluginData) { for (const key of [ "secrets", "variables", "routes", "sidebar", "production", "integrity", "proxy" ]) if (pluginData[key] !== void 0 && entry[key] === void 0) { entry[key] = pluginData[key]; changed = true; } if (typeof pluginData.development === "string" && pluginData.development.startsWith("local:")) pluginData.development = `local:plugins/${pluginKey}`; if (entry.development === void 0 && pluginData.development !== void 0) { entry.development = pluginData.development; changed = true; } } if (apiData) { for (const key of [ "production", "integrity", "proxy", "variables", "secrets", "sidebar", "routes" ]) if (apiData[key] !== void 0 && entry[key] === void 0) { entry[key] = apiData[key]; changed = true; } } if ("extends" in entry) { const extendsStr = typeof entry.extends === "string" ? entry.extends : void 0; if (!extendsStr || extendsStr.includes(`#plugins.${pluginKey}`)) { delete entry.extends; changed = true; } } if ("name" in entry) { delete entry.name; changed = true; } if (configHasTopLevelFields(pluginConfig, pluginKey)) { if (entry.routes === void 0 && Array.isArray(pluginConfig.routes)) { entry.routes = pluginConfig.routes; changed = true; } if (entry.sidebar === void 0 && Array.isArray(pluginConfig.sidebar)) { entry.sidebar = pluginConfig.sidebar; changed = true; } const api = getApiEntry(pluginConfig); if (api) { if (entry.routes === void 0 && Array.isArray(api.routes)) { entry.routes = api.routes; changed = true; } if (entry.sidebar === void 0 && Array.isArray(api.sidebar)) { entry.sidebar = api.sidebar; changed = true; } } } return changed; } function extractPluginEntry(pluginConfig, pluginKey) { if (pluginConfig.plugins && typeof pluginConfig.plugins === "object" && pluginConfig.plugins[pluginKey] && typeof pluginConfig.plugins[pluginKey] === "object") return pluginConfig.plugins[pluginKey]; const fallback = {}; if (pluginConfig.sidebar !== void 0) fallback.sidebar = pluginConfig.sidebar; if (pluginConfig.routes !== void 0) fallback.routes = pluginConfig.routes; if (Object.keys(fallback).length > 0) return fallback; return null; } function configHasTopLevelFields(pluginConfig, _pluginKey) { return pluginConfig.routes !== void 0 && Array.isArray(pluginConfig.routes) || pluginConfig.sidebar !== void 0 && Array.isArray(pluginConfig.sidebar) || getApiEntry(pluginConfig) !== null; } function getApiEntry(pluginConfig) { if (!pluginConfig.app || typeof pluginConfig.app !== "object") return null; const app = pluginConfig.app; if (!app.api || typeof app.api !== "object") return null; return app.api; } async function migrateBosConfigFiles(projectDir) { const migrated = []; const rootConfigPath = join(projectDir, "bos.config.json"); if (existsSync(rootConfigPath)) { const rootConfig = JSON.parse(readFileSync(rootConfigPath, "utf-8")); let rootChanged = migrateRootConfigTargets(rootConfig); const pluginConfigPaths = await glob("plugins/*/bos.config.json", { cwd: projectDir, nodir: true, dot: false, absolute: false }); for (const relativePath of pluginConfigPaths) { const pluginKey = relativePath.match(/^plugins\/([^/]+)\/bos\.config\.json$/)?.[1]; if (!pluginKey) continue; const filePath = join(projectDir, relativePath); try { rootChanged = mergePluginConfigIntoRoot(rootConfig, pluginKey, JSON.parse(readFileSync(filePath, "utf-8"))) || rootChanged; } catch {} try { rmSync(filePath); migrated.push(relativePath); } catch {} } if (rootConfig.plugins && typeof rootConfig.plugins === "object") for (const pluginKey of Object.keys(rootConfig.plugins)) rootChanged = migratePluginProviderConfig(rootConfig, pluginKey) || rootChanged; if (rootChanged || migrated.length > 0) { await saveBosConfig(projectDir, rootConfig); if (!migrated.includes("bos.config.json")) migrated.push("bos.config.json"); } } return migrated; } async function loadParentPluginOptions(projectDir) { const configPath = join(projectDir, "bos.config.json"); if (!existsSync(configPath)) return null; const localConfig = JSON.parse(readFileSync(configPath, "utf-8")); const extendsRef = getExtendsRef(localConfig); if (!extendsRef?.startsWith("bos://")) return null; const parsed = parseBosRef(extendsRef); if (!parsed) return null; let parentConfig; try { parentConfig = await fetchParentConfig(parsed.account, parsed.gateway); } catch { return null; } const parentPlugins = parentConfig.plugins && typeof parentConfig.plugins === "object" ? parentConfig.plugins : {}; const localPlugins = localConfig.plugins && typeof localConfig.plugins === "object" ? localConfig.plugins : {}; return { localConfig, parentPlugins, newPluginKeys: Object.keys(parentPlugins).filter((key) => !(key in localPlugins)) }; } async function addSelectedParentPlugins(projectDir) { if (!process.stdin.isTTY || !process.stdout.isTTY) return []; const pluginOptions = await loadParentPluginOptions(projectDir); if (!pluginOptions || pluginOptions.newPluginKeys.length === 0) return []; const selectedValue = await p.multiselect({ message: "Select new plugins from parent:", options: pluginOptions.newPluginKeys.map((key) => ({ value: key, label: key })), required: false }); if (p.isCancel(selectedValue)) process.exit(0); const selected = selectedValue; if (selected.length === 0) return []; const nextPlugins = { ...pluginOptions.localConfig.plugins && typeof pluginOptions.localConfig.plugins === "object" ? pluginOptions.localConfig.plugins : {} }; for (const key of selected) { const parentPlugin = pluginOptions.parentPlugins[key]; if (parentPlugin && typeof parentPlugin === "object") { const nextPlugin = structuredClone(parentPlugin); rewriteExtendsTarget(nextPlugin, `plugins.${key}`); nextPlugins[key] = nextPlugin; } else if (typeof parentPlugin === "string") nextPlugins[key] = ensureTargetedRef(parentPlugin, `plugins.${key}`); else nextPlugins[key] = parentPlugin; } pluginOptions.localConfig.plugins = nextPlugins; await saveBosConfig(projectDir, pluginOptions.localConfig); return selected; } function readInstalledVersion(projectDir, packageName) { return readInstalledFrameworkVersion(projectDir, packageName); } function setCatalogRef(field, packageName) { if (!field || !(packageName in field)) return false; if (field[packageName] === "catalog:" || field[packageName].startsWith("file:")) return false; field[packageName] = "catalog:"; return true; } function updateWorkspacePackageRefInFile(filePath, packageName) { const pkg = JSON.parse(readFileSync(filePath, "utf-8")); let modified = false; for (const fieldName of [ "dependencies", "devDependencies", "peerDependencies" ]) { const field = pkg[fieldName]; if (setCatalogRef(field, packageName)) modified = true; } if (modified) writeFileSync(filePath, `${JSON.stringify(pkg, null, 2)}\n`); return modified; } function updateRootPackageVersion(projectDir, packageName, newVersion) { const pkgPath = join(projectDir, "package.json"); const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); let modified = false; for (const fieldName of [ "dependencies", "devDependencies", "peerDependencies" ]) { const field = pkg[fieldName]; if (setCatalogRef(field, packageName)) modified = true; } if (!pkg.workspaces || typeof pkg.workspaces !== "object") { pkg.workspaces = { packages: [], catalog: {} }; modified = true; } const workspaces = pkg.workspaces; if (!workspaces.catalog || typeof workspaces.catalog !== "object") { workspaces.catalog = {}; modified = true; } const nextVersion = newVersion; if (workspaces.catalog[packageName] !== nextVersion) { workspaces.catalog[packageName] = nextVersion; modified = true; } if (modified) writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); return modified; } function updateRootCatalogVersion(projectDir, packageName, newVersion) { const pkgPath = join(projectDir, "package.json"); const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")); if (!pkg.workspaces || typeof pkg.workspaces !== "object") pkg.workspaces = { packages: [], catalog: {} }; const workspaces = pkg.workspaces; if (!workspaces.catalog || typeof workspaces.catalog !== "object") workspaces.catalog = {}; const nextVersion = newVersion; if (workspaces.catalog[packageName] === nextVersion) return false; workspaces.catalog[packageName] = nextVersion; writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`); return true; } async function findWorkspacePackageJsons(projectDir) { const rootPkgPath = join(projectDir, "package.json"); if (!existsSync(rootPkgPath)) return []; const workspaceConfig = JSON.parse(readFileSync(rootPkgPath, "utf-8")).workspaces; const patterns = []; if (Array.isArray(workspaceConfig)) patterns.push(...workspaceConfig); else if (workspaceConfig?.packages && Array.isArray(workspaceConfig.packages)) patterns.push(...workspaceConfig.packages); if (patterns.length === 0) return []; const pkgPaths = []; for (const pattern of patterns) { const matches = await glob(pattern, { cwd: projectDir, dot: false, absolute: false }); for (const match of matches) { const pkgPath = join(projectDir, match, "package.json"); if (existsSync(pkgPath) && statSync(pkgPath).isFile()) pkgPaths.push(pkgPath); } } return [...new Set(pkgPaths)]; } function buildChangelogUrl(oldVersion, newVersion, parentConfig) { if (!oldVersion || oldVersion === newVersion) return void 0; const repoUrl = parentConfig?.repository; if (!repoUrl) return void 0; const githubMatch = repoUrl.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/); if (!githubMatch) return void 0; const [, owner, repo] = githubMatch; return `https://github.com/${owner}/${repo}/compare/v${oldVersion}...v${newVersion}`; } async function rewriteLegacyUiImports(projectDir) { const files = await glob("ui/src/**/*.{ts,tsx}", { cwd: projectDir, nodir: true, dot: false, absolute: false }); const migrated = []; for (const file of files) { const filePath = join(projectDir, file); const original = readFileSync(filePath, "utf-8"); let next = original; for (const [from, to] of LEGACY_UI_IMPORT_REWRITES) next = next.replaceAll(from, to); if (next !== original) { writeFileSync(filePath, next); migrated.push(file); } } return migrated; } async function upgradeTemplate(projectDir, options) { const timings = []; if (!existsSync(join(projectDir, "package.json"))) return { status: "error", packages: [], timings, error: "No package.json found in current directory" }; const sourceRootCatalog = await readExtendedRootCatalog(projectDir); const { packages, catalogVersionUpdates } = await timePhase(timings, "check package versions", async () => { const nextPackages = []; for (const name of FRAMEWORK_PACKAGES) { const installed = readInstalledVersion(projectDir, name); const latest = extractVersion(sourceRootCatalog[name]); if (!latest) { nextPackages.push({ name, from: installed, to: installed ?? "unknown" }); continue; } nextPackages.push({ name, from: installed, to: latest }); } const nextCatalogVersionUpdates = []; for (const name of CATALOG_TOOL_PACKAGES) { const installed = readInstalledVersion(projectDir, name); if (!installed) continue; const targetVersion = PINNED_CATALOG_TOOL_VERSIONS[name] ?? extractVersion(sourceRootCatalog[name]) ?? installed; if (installed === targetVersion) continue; nextCatalogVersionUpdates.push({ name, from: installed, to: targetVersion }); } return { packages: nextPackages, catalogVersionUpdates: nextCatalogVersionUpdates }; }); const hasFrameworkUpdates = packages.some((p) => p.from !== p.to && p.from !== void 0); const hasCatalogUpdates = catalogVersionUpdates.length > 0; const hasUpdates = hasFrameworkUpdates || hasCatalogUpdates; if (options.dryRun) { let changelogUrl; const pluginOptions = options.noSync ? null : await timePhase(timings, "discover parent plugins", () => loadParentPluginOptions(projectDir)); if (hasUpdates) { const configPath = join(projectDir, "bos.config.json"); let parentConfig = null; if (existsSync(configPath)) try { parentConfig = JSON.parse(readFileSync(configPath, "utf-8")); } catch {} const mainPkg = packages.find((p) => p.name === "everything-dev"); if (mainPkg?.from && mainPkg.from !== mainPkg.to) changelogUrl = buildChangelogUrl(mainPkg.from, mainPkg.to, parentConfig); } return { status: "dry-run", packages: [...packages, ...catalogVersionUpdates.map((u) => ({ name: u.name, from: u.from, to: u.to }))], availablePlugins: pluginOptions?.newPluginKeys, timings, changelogUrl }; } await timePhase(timings, "apply package updates", async () => { for (const pkg of packages) if (pkg.from !== void 0 && pkg.from !== pkg.to) updateRootPackageVersion(projectDir, pkg.name, pkg.to); for (const update of catalogVersionUpdates) updateRootCatalogVersion(projectDir, update.name, update.to); const workspacePkgPaths = await findWorkspacePackageJsons(projectDir); for (const pkgPath of workspacePkgPaths) { for (const pkg of packages) if (pkg.from !== void 0 && pkg.from !== pkg.to) updateWorkspacePackageRefInFile(pkgPath, pkg.name); for (const update of catalogVersionUpdates) updateWorkspacePackageRefInFile(pkgPath, update.name); } }); const migratedBosConfigs = await timePhase(timings, "migrate bos configs", () => migrateBosConfigFiles(projectDir)); let syncResult; let addedPlugins = []; if (!options.noSync) { addedPlugins = await timePhase(timings, "discover parent plugins", async () => { if (options.dryRun) return []; return addSelectedParentPlugins(projectDir); }); syncResult = await timePhase(timings, "sync template", () => syncTemplate(projectDir, { dryRun: false, force: options.force, noInstall: true })); } if ((hasUpdates || addedPlugins.length > 0) && !options.noInstall) { await timePhase(timings, "install dependencies", () => runBunInstallForUpgrade(projectDir)); await timePhase(timings, "generate types", () => runTypesGen(projectDir)); } const migratedFiles = await timePhase(timings, "clean obsolete files", async () => { const nextMigratedFiles = [...migratedBosConfigs, ...await rewriteLegacyUiImports(projectDir)]; for (const file of OBSOLETE_FILES) { const filePath = join(projectDir, file); if (existsSync(filePath)) { rmSync(filePath); nextMigratedFiles.push(file); } } return nextMigratedFiles; }); let changelogUrl; const mainPkg = packages.find((p) => p.name === "everything-dev"); if (mainPkg?.from && mainPkg.from !== mainPkg.to) { const configPath = join(projectDir, "bos.config.json"); let parentConfig = null; if (existsSync(configPath)) try { parentConfig = JSON.parse(readFileSync(configPath, "utf-8")); } catch {} changelogUrl = buildChangelogUrl(mainPkg.from, mainPkg.to, parentConfig); } return { status: "upgraded", packages: [...packages, ...catalogVersionUpdates.map((u) => ({ name: u.name, from: u.from, to: u.to }))], sync: syncResult, migrated: migratedFiles.length > 0 ? migratedFiles : void 0, selectedPlugins: addedPlugins.length > 0 ? addedPlugins : void 0, timings, changelogUrl }; } //#endregion export { upgradeTemplate }; //# sourceMappingURL=upgrade.mjs.map