diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..41583e36ca --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/packages/tools/package.json b/packages/tools/package.json index 370470bbf6..167034708a 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -7,7 +7,12 @@ "json-edit": "./src/json-edit.ts" }, "devDependencies": { - "minimatch": "catalog:" + "@oxc-node/cli": "catalog:", + "@oxc-node/core": "catalog:", + "@types/semver": "catalog:", + "@std/yaml": "catalog:", + "minimatch": "catalog:", + "semver": "catalog:" }, "scripts": { "snap-test": "tool snap-test" diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts index 796315ee8f..6b93612d2e 100644 --- a/packages/tools/src/index.ts +++ b/packages/tools/src/index.ts @@ -1,17 +1,20 @@ -import { replaceFileContent } from './replace-file-content'; -import { snapTest } from './snap-test'; - const subcommand = process.argv[2]; switch (subcommand) { case 'snap-test': + const { snapTest } = await import('./snap-test'); await snapTest(); break; case 'replace-file-content': + const { replaceFileContent } = await import('./replace-file-content'); replaceFileContent(); break; + case 'sync-remote': + const { syncRemote } = await import('./sync-remote-deps'); + syncRemote(); + break; default: console.error(`Unknown subcommand: ${subcommand}`); - console.error('Available subcommands: snap-test, replace-file-content'); + console.error('Available subcommands: snap-test, replace-file-content, sync-remote'); process.exit(1); } diff --git a/packages/tools/src/sync-remote-deps.ts b/packages/tools/src/sync-remote-deps.ts new file mode 100755 index 0000000000..b1c080a306 --- /dev/null +++ b/packages/tools/src/sync-remote-deps.ts @@ -0,0 +1,365 @@ +import { parse as parseYaml, stringify as stringifyYaml } from '@std/yaml'; +import { execSync, spawnSync } from 'node:child_process'; +import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { parseArgs } from 'node:util'; +import * as semver from 'semver'; + +interface PnpmWorkspace { + packages?: string[]; + catalog?: Record; + catalogMode?: string; + minimumReleaseAge?: number; + minimumReleaseAgeExclude?: string[]; + patchedDependencies?: Record; + peerDependencyRules?: { + allowedVersions?: Record; + }; + packageExtensions?: Record; + overrides?: Record; + ignoreScripts?: boolean; + [key: string]: any; +} + +const ROLLDOWN_REPO = 'git@github.com:rolldown/rolldown.git'; +const ROLLDOWN_VITE_REPO = 'git@github.com:vitejs/rolldown-vite.git'; +const ROLLDOWN_DIR = 'rolldown'; +const ROLLDOWN_VITE_DIR = 'rolldown-vite'; +const ROLLDOWN_VITE_BRANCH = 'rolldown-vite'; + +function log(message: string) { + console.log(`[sync-rolldown] ${message}`); +} + +function error(message: string): never { + console.error(`[sync-rolldown] ERROR: ${message}`); + process.exit(1); +} + +function execCommand(command: string, cwd?: string): string { + try { + return execSync(command, { + cwd, + encoding: 'utf-8', + stdio: 'pipe', + }).trim(); + } catch (err: any) { + throw new Error(`Failed to execute: ${command}\n${err.message}`); + } +} + +function cloneOrResetRepo( + repoUrl: string, + dir: string, + branch: string = 'main', +) { + log(`Processing ${dir}...`); + + if (existsSync(dir)) { + log(`${dir} exists, checking git status...`); + try { + // Check if it's a valid git repo + const result = spawnSync('git', ['rev-parse', '--git-dir'], { + cwd: dir, + encoding: 'utf-8', + }); + + if (result.status !== 0) { + log(`${dir} is not a valid git repo, removing and re-cloning...`); + rmSync(dir, { recursive: true, force: true }); + cloneRepo(repoUrl, dir, branch); + return; + } + + // Check remote URL + const remoteUrl = execCommand('git remote get-url origin', dir); + if (remoteUrl !== repoUrl) { + log( + `${dir} has wrong remote (${remoteUrl} vs ${repoUrl}), removing and re-cloning...`, + ); + rmSync(dir, { recursive: true, force: true }); + cloneRepo(repoUrl, dir, branch); + return; + } + + // Reset to latest + log(`Resetting ${dir} to latest ${branch}...`); + execCommand('git fetch origin', dir); + execCommand(`git checkout ${branch}`, dir); + execCommand(`git reset --hard origin/${branch}`, dir); + execCommand('git clean -fdx', dir); + log(`${dir} reset to latest ${branch}`); + } catch (err: any) { + log( + `Failed to reset ${dir} (${err.message}), removing and re-cloning...`, + ); + rmSync(dir, { recursive: true, force: true }); + cloneRepo(repoUrl, dir, branch); + } + } else { + cloneRepo(repoUrl, dir, branch); + } +} + +function cloneRepo(repoUrl: string, dir: string, branch: string) { + log(`Cloning ${repoUrl} (${branch}) into ${dir}...`); + execCommand(`git clone --branch ${branch} ${repoUrl} ${dir}`); + log(`${dir} cloned successfully`); +} + +function mergeSemverVersions(v1: string, v2: string, packageName: string): string { + // Handle special cases + if (v1 === v2) return v1; + + // Handle exact version specifiers (=) + const isExact1 = v1.startsWith('='); + const isExact2 = v2.startsWith('='); + if (isExact1 || isExact2) { + if (isExact1 && isExact2 && v1 !== v2) { + error( + `Incompatible exact versions for ${packageName}: ${v1} vs ${v2}`, + ); + } + return isExact1 ? v1 : v2; + } + + // Handle npm: prefix + if (v1.startsWith('npm:') || v2.startsWith('npm:')) { + // If one has npm: prefix, prefer the non-npm version or return the first one + if (!v1.startsWith('npm:')) return v1; + if (!v2.startsWith('npm:')) return v2; + return v1; + } + + // Parse version ranges + const range1 = semver.validRange(v1); + const range2 = semver.validRange(v2); + + if (!range1 || !range2) { + log(`Warning: Could not parse semver for ${packageName}: ${v1}, ${v2}. Using ${v1}`); + return v1; + } + + // Get the major versions from the ranges + const getMajor = (range: string): number | null => { + const match = range.match(/(\d+)\./); + return match ? parseInt(match[1], 10) : null; + }; + + const major1 = getMajor(v1); + const major2 = getMajor(v2); + + if (major1 === null || major2 === null) { + return v1; + } + + // Check if major versions are compatible + if (major1 !== major2) { + error( + `Incompatible semver ranges for ${packageName}: ${v1} (major: ${major1}) vs ${v2} (major: ${major2})`, + ); + } + + // Both have same major version, return the higher one + // Compare the minimum versions + const minVersion1 = semver.minVersion(range1); + const minVersion2 = semver.minVersion(range2); + + if (minVersion1 && minVersion2) { + if (semver.gt(minVersion1, minVersion2)) { + return v1; + } else if (semver.gt(minVersion2, minVersion1)) { + return v2; + } + } + + return v1; +} + +function mergePnpmWorkspaces( + main: PnpmWorkspace, + rolldown: PnpmWorkspace, + rolldownVite: PnpmWorkspace, +): PnpmWorkspace { + const result: PnpmWorkspace = { ...main }; + + // Merge packages array + const packagesSet = new Set(main.packages || []); + // Add rolldown packages + packagesSet.add(ROLLDOWN_DIR); + packagesSet.add(`${ROLLDOWN_DIR}/packages/*`); + // Add rolldown-vite packages + packagesSet.add(ROLLDOWN_VITE_DIR); + packagesSet.add(`${ROLLDOWN_VITE_DIR}/packages/*`); + result.packages = Array.from(packagesSet); + + // Merge catalog + const catalog: Record = { ...main.catalog }; + + // Add all entries from rolldown catalog + for (const [pkg, version] of Object.entries(rolldown.catalog || {})) { + if (pkg === 'rolldown' || pkg === 'rolldown-vite') { + // Force workspace:* for rolldown packages + catalog[pkg] = 'workspace:*'; + } else if (catalog[pkg]) { + // Merge versions + catalog[pkg] = mergeSemverVersions(catalog[pkg], version, pkg); + } else { + catalog[pkg] = version; + } + } + + // Add all entries from rolldown-vite catalog (if it has one) + for (const [pkg, version] of Object.entries(rolldownVite.catalog || {})) { + if (pkg === 'rolldown' || pkg === 'rolldown-vite') { + // Force workspace:* for rolldown packages + catalog[pkg] = 'workspace:*'; + } else if (catalog[pkg]) { + // Merge versions + catalog[pkg] = mergeSemverVersions(catalog[pkg], version, pkg); + } else { + catalog[pkg] = version; + } + } + + result.catalog = catalog; + + // Merge minimumReleaseAgeExclude + const excludeSet = new Set(main.minimumReleaseAgeExclude || []); + excludeSet.add('@napi-rs/*'); + (rolldown.minimumReleaseAgeExclude || []).forEach((item) => excludeSet.add(item)); + (rolldownVite.minimumReleaseAgeExclude || []).forEach((item) => excludeSet.add(item)); + result.minimumReleaseAgeExclude = Array.from(excludeSet); + + // Copy patchedDependencies from rolldown-vite (with path prefix) + if (rolldownVite.patchedDependencies) { + result.patchedDependencies = {}; + for ( + const [dep, patchPath] of Object.entries( + rolldownVite.patchedDependencies, + ) + ) { + // Prepend rolldown-vite directory to patch paths + result.patchedDependencies[dep] = patchPath.startsWith('./') + ? `./${ROLLDOWN_VITE_DIR}/${patchPath.slice(2)}` + : `${ROLLDOWN_VITE_DIR}/${patchPath}`; + } + } + + // Merge peerDependencyRules + if (rolldownVite.peerDependencyRules) { + result.peerDependencyRules = { + ...main.peerDependencyRules, + allowedVersions: { + ...main.peerDependencyRules?.allowedVersions, + ...rolldownVite.peerDependencyRules.allowedVersions, + }, + }; + // Add rolldown to allowed versions + if (result.peerDependencyRules.allowedVersions) { + result.peerDependencyRules.allowedVersions.rolldown = '*'; + } + } + + // Copy packageExtensions from rolldown-vite + if (rolldownVite.packageExtensions) { + result.packageExtensions = { + ...main.packageExtensions, + ...rolldownVite.packageExtensions, + }; + } + + // Update overrides + result.overrides = { + ...main.overrides, + rolldown: 'workspace:*', + vite: `./${ROLLDOWN_VITE_DIR}/packages/vite`, + }; + + // Set ignoreScripts + result.ignoreScripts = true; + + return result; +} + +export function syncRemote() { + const { values } = parseArgs({ + options: { + 'clean': { + type: 'boolean', + }, + }, + args: process.argv.slice(3), + }); + + log('Starting rolldown/rolldown-vite sync...'); + + // Get the root directory (assuming script is run from root) + const rootDir = process.cwd(); + + if (values.clean) { + log('Cleaning existing repositories...'); + if (existsSync(join(rootDir, ROLLDOWN_DIR))) { + rmSync(join(rootDir, ROLLDOWN_DIR), { recursive: true, force: true }); + log(`Removed ${ROLLDOWN_DIR}`); + } + if (existsSync(join(rootDir, ROLLDOWN_VITE_DIR))) { + rmSync(join(rootDir, ROLLDOWN_VITE_DIR), { recursive: true, force: true }); + log(`Removed ${ROLLDOWN_VITE_DIR}`); + } + } + + // Clone or reset repos + cloneOrResetRepo(ROLLDOWN_REPO, join(rootDir, ROLLDOWN_DIR), 'main'); + cloneOrResetRepo( + ROLLDOWN_VITE_REPO, + join(rootDir, ROLLDOWN_VITE_DIR), + ROLLDOWN_VITE_BRANCH, + ); + + log('Reading pnpm-workspace.yaml files...'); + + // Read main pnpm-workspace.yaml + const mainWorkspacePath = join(rootDir, 'pnpm-workspace.yaml'); + const mainWorkspace = parseYaml( + readFileSync(mainWorkspacePath, 'utf-8'), + ) as PnpmWorkspace; + + // Read rolldown pnpm-workspace.yaml + const rolldownWorkspacePath = join( + rootDir, + ROLLDOWN_DIR, + 'pnpm-workspace.yaml', + ); + const rolldownWorkspace = parseYaml( + readFileSync(rolldownWorkspacePath, 'utf-8'), + ) as PnpmWorkspace; + + // Read rolldown-vite pnpm-workspace.yaml + const rolldownViteWorkspacePath = join( + rootDir, + ROLLDOWN_VITE_DIR, + 'pnpm-workspace.yaml', + ); + const rolldownViteWorkspace = parseYaml( + readFileSync(rolldownViteWorkspacePath, 'utf-8'), + ) as PnpmWorkspace; + + log('Merging pnpm-workspace.yaml files...'); + + const mergedWorkspace = mergePnpmWorkspaces( + mainWorkspace, + rolldownWorkspace, + rolldownViteWorkspace, + ); + + // Write the merged workspace back + const yamlContent = stringifyYaml(mergedWorkspace, { + lineWidth: -1, + }); + + writeFileSync(mainWorkspacePath, yamlContent, 'utf-8'); + + log('✓ pnpm-workspace.yaml updated successfully!'); + log('✓ Done!'); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 17b3a24664..5dc24ee7b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,12 +18,18 @@ catalogs: '@oxc-node/core': specifier: ^0.0.32 version: 0.0.32 + '@std/yaml': + specifier: npm:@jsr/std__yaml@^1.0.10 + version: 1.0.10 '@types/cross-spawn': specifier: ^6.0.6 version: 6.0.6 '@types/node': specifier: ^24.9.1 version: 24.9.1 + '@types/semver': + specifier: ^7.7.1 + version: 7.7.1 '@vitest/browser': specifier: ^4.0.4 version: 4.0.4 @@ -72,6 +78,9 @@ catalogs: rolldown-vite: specifier: ^7.1.19 version: 7.1.19 + semver: + specifier: ^7.7.3 + version: 7.7.3 tsdown: specifier: ^0.15.10 version: 0.15.10 @@ -246,9 +255,24 @@ importers: packages/tools: devDependencies: + '@oxc-node/cli': + specifier: 'catalog:' + version: 0.0.32 + '@oxc-node/core': + specifier: 'catalog:' + version: 0.0.32 + '@std/yaml': + specifier: 'catalog:' + version: '@jsr/std__yaml@1.0.10' + '@types/semver': + specifier: 'catalog:' + version: 7.7.1 minimatch: specifier: 'catalog:' version: 10.0.3 + semver: + specifier: 'catalog:' + version: 7.7.3 packages: @@ -602,6 +626,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jsr/std__yaml@1.0.10': + resolution: {integrity: sha512-1WIM023Kvi48pvPE3UO5YcieambLgywUooLhAkkaObIcMB77F/YP2ILdl+vNfik+vElkl9znmuST9AZo8mbCpA==, tarball: https://npm.jsr.io/~/11/@jsr/std__yaml/1.0.10.tgz} + '@napi-rs/cli@3.1.5': resolution: {integrity: sha512-Wn6ZPw27qJiEWglGjkaAa70AHuLtyPya6FvjINYJ5U20uvbRhoB0Ta2+bFTAFfUb9R+wvuFvog9JQdy65OmFAQ==} engines: {node: '>= 16'} @@ -1479,6 +1506,9 @@ packages: '@types/node@24.9.1': resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -2901,6 +2931,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jsr/std__yaml@1.0.10': {} + '@napi-rs/cli@3.1.5(@emnapi/runtime@1.6.0)(@types/node@24.9.1)': dependencies: '@inquirer/prompts': 7.6.0(@types/node@24.9.1) @@ -3541,6 +3573,8 @@ snapshots: dependencies: undici-types: 7.16.0 + '@types/semver@7.7.1': {} + '@types/unist@3.0.3': {} '@types/web-bluetooth@0.0.21': {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 871118ed97..f6cee18abe 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,10 +7,12 @@ catalog: '@napi-rs/cli': ^3.1.5 '@oxc-node/cli': ^0.0.32 '@oxc-node/core': ^0.0.32 + '@std/yaml': npm:@jsr/std__yaml@^1.0.10 '@types/cross-spawn': ^6.0.6 '@types/node': ^24.9.1 '@types/react': ^19.1.8 '@types/react-dom': ^19.1.6 + '@types/semver': ^7.7.1 '@vitest/browser': ^4.0.4 '@vitest/browser-playwright': ^4.0.4 create-tsdown: 0.0.3 @@ -31,6 +33,7 @@ catalog: react-dom: ^19.1.0 rolldown: ^1.0.0-beta.45 rolldown-vite: ^7.1.19 + semver: ^7.7.3 tsdown: ^0.15.10 typescript: ^5.9.3 vite: npm:rolldown-vite@^7.1.19