everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
609 lines (607 loc) • 23.1 kB
JavaScript
const require_runtime = require('../_virtual/_rolldown/runtime.cjs');
const require_merge = require('../merge.cjs');
const require_save_config = require('../utils/save-config.cjs');
const require_cli_init = require('./init.cjs');
const require_framework_version = require('./framework-version.cjs');
const require_sync = require('./sync.cjs');
const require_timing = require('./timing.cjs');
let node_fs = require("node:fs");
let node_path = require("node:path");
let node_process = require("node:process");
node_process = require_runtime.__toESM(node_process, 1);
let _clack_prompts = require("@clack/prompts");
_clack_prompts = require_runtime.__toESM(_clack_prompts, 1);
let glob = require("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 = (0, node_path.join)(projectDir, "bos.config.json");
if (!(0, node_fs.existsSync)(configPath)) return {};
const localConfig = JSON.parse((0, node_fs.readFileSync)(configPath, "utf-8"));
let extendsRef;
if (typeof localConfig.extends === "string") extendsRef = localConfig.extends;
else if (require_merge.isPlainObject(localConfig.extends)) extendsRef = require_merge.resolveExtendsRef(localConfig.extends, "production");
const parsed = extendsRef ? parseBosRef(extendsRef) : null;
if (!parsed) return {};
const { sourceDir, cleanup } = await require_cli_init.resolveSourceDir({
extendsAccount: parsed.account,
extendsGateway: parsed.gateway
});
try {
const sourcePkgPath = (0, node_path.join)(sourceDir, "package.json");
if (!(0, node_fs.existsSync)(sourcePkgPath)) return {};
return { ...JSON.parse((0, node_fs.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 require_merge.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 = (0, node_path.join)(projectDir, "bos.config.json");
if ((0, node_fs.existsSync)(rootConfigPath)) {
const rootConfig = JSON.parse((0, node_fs.readFileSync)(rootConfigPath, "utf-8"));
let rootChanged = migrateRootConfigTargets(rootConfig);
const pluginConfigPaths = await (0, glob.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 = (0, node_path.join)(projectDir, relativePath);
try {
rootChanged = mergePluginConfigIntoRoot(rootConfig, pluginKey, JSON.parse((0, node_fs.readFileSync)(filePath, "utf-8"))) || rootChanged;
} catch {}
try {
(0, node_fs.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 require_save_config.saveBosConfig(projectDir, rootConfig);
if (!migrated.includes("bos.config.json")) migrated.push("bos.config.json");
}
}
return migrated;
}
async function loadParentPluginOptions(projectDir) {
const configPath = (0, node_path.join)(projectDir, "bos.config.json");
if (!(0, node_fs.existsSync)(configPath)) return null;
const localConfig = JSON.parse((0, node_fs.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 require_cli_init.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 (!node_process.default.stdin.isTTY || !node_process.default.stdout.isTTY) return [];
const pluginOptions = await loadParentPluginOptions(projectDir);
if (!pluginOptions || pluginOptions.newPluginKeys.length === 0) return [];
const selectedValue = await _clack_prompts.multiselect({
message: "Select new plugins from parent:",
options: pluginOptions.newPluginKeys.map((key) => ({
value: key,
label: key
})),
required: false
});
if (_clack_prompts.isCancel(selectedValue)) node_process.default.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 require_save_config.saveBosConfig(projectDir, pluginOptions.localConfig);
return selected;
}
function readInstalledVersion(projectDir, packageName) {
return require_framework_version.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((0, node_fs.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) (0, node_fs.writeFileSync)(filePath, `${JSON.stringify(pkg, null, 2)}\n`);
return modified;
}
function updateRootPackageVersion(projectDir, packageName, newVersion) {
const pkgPath = (0, node_path.join)(projectDir, "package.json");
const pkg = JSON.parse((0, node_fs.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) (0, node_fs.writeFileSync)(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
return modified;
}
function updateRootCatalogVersion(projectDir, packageName, newVersion) {
const pkgPath = (0, node_path.join)(projectDir, "package.json");
const pkg = JSON.parse((0, node_fs.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;
(0, node_fs.writeFileSync)(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
return true;
}
async function findWorkspacePackageJsons(projectDir) {
const rootPkgPath = (0, node_path.join)(projectDir, "package.json");
if (!(0, node_fs.existsSync)(rootPkgPath)) return [];
const workspaceConfig = JSON.parse((0, node_fs.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 (0, glob.glob)(pattern, {
cwd: projectDir,
dot: false,
absolute: false
});
for (const match of matches) {
const pkgPath = (0, node_path.join)(projectDir, match, "package.json");
if ((0, node_fs.existsSync)(pkgPath) && (0, node_fs.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 (0, glob.glob)("ui/src/**/*.{ts,tsx}", {
cwd: projectDir,
nodir: true,
dot: false,
absolute: false
});
const migrated = [];
for (const file of files) {
const filePath = (0, node_path.join)(projectDir, file);
const original = (0, node_fs.readFileSync)(filePath, "utf-8");
let next = original;
for (const [from, to] of LEGACY_UI_IMPORT_REWRITES) next = next.replaceAll(from, to);
if (next !== original) {
(0, node_fs.writeFileSync)(filePath, next);
migrated.push(file);
}
}
return migrated;
}
async function upgradeTemplate(projectDir, options) {
const timings = [];
if (!(0, node_fs.existsSync)((0, node_path.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 require_timing.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 require_timing.timePhase(timings, "discover parent plugins", () => loadParentPluginOptions(projectDir));
if (hasUpdates) {
const configPath = (0, node_path.join)(projectDir, "bos.config.json");
let parentConfig = null;
if ((0, node_fs.existsSync)(configPath)) try {
parentConfig = JSON.parse((0, node_fs.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 require_timing.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 require_timing.timePhase(timings, "migrate bos configs", () => migrateBosConfigFiles(projectDir));
let syncResult;
let addedPlugins = [];
if (!options.noSync) {
addedPlugins = await require_timing.timePhase(timings, "discover parent plugins", async () => {
if (options.dryRun) return [];
return addSelectedParentPlugins(projectDir);
});
syncResult = await require_timing.timePhase(timings, "sync template", () => require_sync.syncTemplate(projectDir, {
dryRun: false,
force: options.force,
noInstall: true
}));
}
if ((hasUpdates || addedPlugins.length > 0) && !options.noInstall) {
await require_timing.timePhase(timings, "install dependencies", () => require_cli_init.runBunInstallForUpgrade(projectDir));
await require_timing.timePhase(timings, "generate types", () => require_cli_init.runTypesGen(projectDir));
}
const migratedFiles = await require_timing.timePhase(timings, "clean obsolete files", async () => {
const nextMigratedFiles = [...migratedBosConfigs, ...await rewriteLegacyUiImports(projectDir)];
for (const file of OBSOLETE_FILES) {
const filePath = (0, node_path.join)(projectDir, file);
if ((0, node_fs.existsSync)(filePath)) {
(0, node_fs.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 = (0, node_path.join)(projectDir, "bos.config.json");
let parentConfig = null;
if ((0, node_fs.existsSync)(configPath)) try {
parentConfig = JSON.parse((0, node_fs.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
exports.upgradeTemplate = upgradeTemplate;
//# sourceMappingURL=upgrade.cjs.map