mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-02 11:06:55 +00:00
@@ -1,4 +1,4 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { spawn } from 'child_process';
|
||||
import { createHash } from 'crypto';
|
||||
import {
|
||||
existsSync,
|
||||
@@ -8,9 +8,13 @@ import {
|
||||
writeFileSync,
|
||||
} from 'fs';
|
||||
import { join, resolve } from 'path';
|
||||
import { cpus } from 'os';
|
||||
|
||||
const CHECKSUM_FILE = '.build-checksums.json';
|
||||
const WORKSPACE_PACKAGES = ['core', ...getPackages()];
|
||||
const ALL_PACKAGES = ['core', ...getPackages()];
|
||||
const BUILD_LEVELS = computeBuildLevels(ALL_PACKAGES);
|
||||
// Flatten levels → stable iteration order for checksum checks
|
||||
const WORKSPACE_PACKAGES = BUILD_LEVELS.flat();
|
||||
|
||||
function getPackages() {
|
||||
try {
|
||||
@@ -29,6 +33,91 @@ function getPackages() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the @noco-integrations/* workspace dependency short-names for a package.
|
||||
* e.g. { "@noco-integrations/smtp-auth": "workspace:*" } → ["smtp-auth"]
|
||||
*/
|
||||
function getWorkspaceDependencies(packagePath) {
|
||||
const pkgJsonPath = join(packagePath, 'package.json');
|
||||
if (!existsSync(pkgJsonPath)) return [];
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
||||
const allDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
||||
return Object.entries(allDeps)
|
||||
.filter(([name, version]) =>
|
||||
name.startsWith('@noco-integrations/') &&
|
||||
String(version).startsWith('workspace:'),
|
||||
)
|
||||
.map(([name]) => name.replace('@noco-integrations/', ''));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns packages grouped into build levels via BFS.
|
||||
* All packages within a level have no unbuilt dependencies — they can be built
|
||||
* in parallel. Level N finishes completely before level N+1 starts.
|
||||
*
|
||||
* Example with 60 packages all depending only on core:
|
||||
* Level 0: [core]
|
||||
* Level 1: [all-60-packages] ← built in parallel
|
||||
*/
|
||||
function computeBuildLevels(packages) {
|
||||
const nameToPath = new Map();
|
||||
for (const pkg of packages) {
|
||||
const shortName = pkg === 'core' ? 'core' : pkg.replace('packages/', '');
|
||||
nameToPath.set(shortName, pkg);
|
||||
}
|
||||
|
||||
const inDegree = new Map();
|
||||
const dependents = new Map();
|
||||
for (const pkg of packages) {
|
||||
inDegree.set(pkg, 0);
|
||||
dependents.set(pkg, []);
|
||||
}
|
||||
for (const pkg of packages) {
|
||||
for (const depName of getWorkspaceDependencies(pkg)) {
|
||||
const depPath = nameToPath.get(depName);
|
||||
if (depPath) {
|
||||
inDegree.set(pkg, inDegree.get(pkg) + 1);
|
||||
dependents.get(depPath).push(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const levels = [];
|
||||
const placed = new Set();
|
||||
let current = packages.filter((pkg) => inDegree.get(pkg) === 0);
|
||||
current.forEach((pkg) => placed.add(pkg));
|
||||
|
||||
while (current.length > 0) {
|
||||
levels.push([...current]);
|
||||
const next = [];
|
||||
for (const pkg of current) {
|
||||
for (const dep of dependents.get(pkg) || []) {
|
||||
inDegree.set(dep, inDegree.get(dep) - 1);
|
||||
if (inDegree.get(dep) === 0 && !placed.has(dep)) {
|
||||
next.push(dep);
|
||||
placed.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
|
||||
// Any unplaced packages have a cycle — build them last with a warning
|
||||
const remaining = packages.filter((pkg) => !placed.has(pkg));
|
||||
if (remaining.length > 0) {
|
||||
console.warn(
|
||||
`⚠️ Cycle detected, building last: ${remaining.join(', ')}`,
|
||||
);
|
||||
levels.push(remaining);
|
||||
}
|
||||
|
||||
return levels;
|
||||
}
|
||||
|
||||
function getAllFiles(dir, files = []) {
|
||||
try {
|
||||
const items = readdirSync(dir);
|
||||
@@ -36,12 +125,7 @@ function getAllFiles(dir, files = []) {
|
||||
const fullPath = join(dir, item);
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
// Skip node_modules and dist directories
|
||||
if (
|
||||
item !== 'node_modules' &&
|
||||
item !== 'dist' &&
|
||||
!item.startsWith('.')
|
||||
) {
|
||||
if (item !== 'node_modules' && item !== 'dist' && !item.startsWith('.')) {
|
||||
getAllFiles(fullPath, files);
|
||||
}
|
||||
} else {
|
||||
@@ -62,19 +146,13 @@ function calculateChecksum(packagePath) {
|
||||
|
||||
const files = getAllFiles(packagePath);
|
||||
const hash = createHash('sha256');
|
||||
|
||||
// Sort files for consistent ordering across platforms
|
||||
files.sort();
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
// Add the relative file path to the hash for structure consistency
|
||||
const relativePath = file.replace(packagePath, '').replace(/\\/g, '/');
|
||||
hash.update(relativePath);
|
||||
|
||||
// Add the file contents
|
||||
const content = readFileSync(file);
|
||||
hash.update(content);
|
||||
hash.update(readFileSync(file));
|
||||
} catch {
|
||||
// Skip files we can't read
|
||||
}
|
||||
@@ -82,10 +160,7 @@ function calculateChecksum(packagePath) {
|
||||
|
||||
return hash.digest('hex');
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error calculating checksum for ${packagePath}:`,
|
||||
error.message,
|
||||
);
|
||||
console.error(`Error calculating checksum for ${packagePath}:`, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -120,20 +195,80 @@ function hasDistFolder(packagePath) {
|
||||
return existsSync(join(packagePath, 'dist'));
|
||||
}
|
||||
|
||||
function buildPackage(packagePath) {
|
||||
console.log(`📦 Building ${packagePath}...`);
|
||||
try {
|
||||
execSync('pnpm build', { cwd: packagePath, stdio: 'inherit' });
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to build ${packagePath}:`, error.message);
|
||||
return false;
|
||||
/**
|
||||
* Builds a single package asynchronously. Output is buffered and printed
|
||||
* atomically when the process exits so parallel builds don't interleave.
|
||||
*/
|
||||
function buildPackageAsync(packagePath) {
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn('pnpm', ['build'], {
|
||||
cwd: packagePath,
|
||||
shell: process.platform === 'win32',
|
||||
});
|
||||
|
||||
const chunks = [];
|
||||
proc.stdout?.on('data', (d) => chunks.push({ stream: 'out', d }));
|
||||
proc.stderr?.on('data', (d) => chunks.push({ stream: 'err', d }));
|
||||
|
||||
proc.on('close', (code) => {
|
||||
const success = code === 0;
|
||||
if (success) {
|
||||
console.log(` ✅ ${packagePath}`);
|
||||
} else {
|
||||
console.error(` ❌ ${packagePath} (exit ${code})`);
|
||||
if (chunks.length) {
|
||||
console.error(` --- output for ${packagePath} ---`);
|
||||
for (const { stream, d } of chunks) {
|
||||
(stream === 'out' ? process.stdout : process.stderr).write(d);
|
||||
}
|
||||
console.error(` --- end ${packagePath} ---`);
|
||||
}
|
||||
}
|
||||
resolve(success);
|
||||
});
|
||||
|
||||
proc.on('error', (err) => {
|
||||
console.error(` ❌ ${packagePath}: spawn error — ${err.message}`);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an array of packages in parallel, capped at maxConcurrent workers.
|
||||
* Returns { successCount, failCount }.
|
||||
*/
|
||||
async function buildLevel(packages, maxConcurrent) {
|
||||
const queue = [...packages];
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
async function worker() {
|
||||
while (queue.length > 0) {
|
||||
const pkg = queue.shift();
|
||||
if (!pkg) break;
|
||||
if (await buildPackageAsync(pkg)) {
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
Array.from({ length: Math.min(maxConcurrent, packages.length) }, worker),
|
||||
);
|
||||
|
||||
return { successCount, failCount };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const force = process.argv.includes('--force');
|
||||
const verbose = process.argv.includes('--verbose');
|
||||
const concurrencyArg = process.argv.find((a) => a.startsWith('--concurrency='));
|
||||
const maxConcurrent = concurrencyArg
|
||||
? Math.max(1, parseInt(concurrencyArg.split('=')[1], 10))
|
||||
: cpus().length;
|
||||
|
||||
if (force) {
|
||||
console.log('🔄 Force rebuild requested, building all packages...');
|
||||
@@ -155,16 +290,13 @@ async function main() {
|
||||
|
||||
const currentChecksum = calculateChecksum(packagePath);
|
||||
if (!currentChecksum) {
|
||||
console.log(
|
||||
`⚠️ Could not calculate checksum for ${packagePath}, skipping...`,
|
||||
);
|
||||
console.log(`⚠️ Could not calculate checksum for ${packagePath}, skipping...`);
|
||||
continue;
|
||||
}
|
||||
|
||||
newChecksums[packagePath] = currentChecksum;
|
||||
|
||||
const oldChecksum = oldChecksums[packagePath];
|
||||
const hasChanged = oldChecksum !== currentChecksum;
|
||||
const hasChanged = oldChecksums[packagePath] !== currentChecksum;
|
||||
const missingDist = !hasDistFolder(packagePath);
|
||||
|
||||
if (force || hasChanged || missingDist) {
|
||||
@@ -183,47 +315,67 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n🚀 Building ${packagesNeedingBuild.length} package(s)...`);
|
||||
const packagesNeedingBuildSet = new Set(packagesNeedingBuild);
|
||||
console.log(
|
||||
`\n🚀 Building ${packagesNeedingBuild.length} package(s) (up to ${maxConcurrent} parallel)...`,
|
||||
);
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
let totalSuccess = 0;
|
||||
let totalFail = 0;
|
||||
|
||||
for (const packagePath of packagesNeedingBuild) {
|
||||
if (buildPackage(packagePath)) {
|
||||
successCount++;
|
||||
} else {
|
||||
failCount++;
|
||||
for (let i = 0; i < BUILD_LEVELS.length; i++) {
|
||||
const toBuild = BUILD_LEVELS[i].filter((pkg) =>
|
||||
packagesNeedingBuildSet.has(pkg),
|
||||
);
|
||||
if (toBuild.length === 0) continue;
|
||||
|
||||
console.log(
|
||||
`\n⚡ Level ${i} — ${toBuild.length} package(s) in parallel`,
|
||||
);
|
||||
if (verbose) {
|
||||
console.log(` ${toBuild.join(', ')}`);
|
||||
}
|
||||
|
||||
const { successCount, failCount } = await buildLevel(toBuild, maxConcurrent);
|
||||
totalSuccess += successCount;
|
||||
totalFail += failCount;
|
||||
|
||||
if (failCount > 0) {
|
||||
console.error(
|
||||
`\n❌ ${failCount} package(s) failed in level ${i} — stopping build`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Save checksums only for successfully built packages
|
||||
if (successCount > 0) {
|
||||
if (totalSuccess > 0) {
|
||||
saveChecksums(newChecksums);
|
||||
console.log(`\n✅ Successfully built ${successCount} package(s)`);
|
||||
console.log(`\n✅ Successfully built ${totalSuccess} package(s)`);
|
||||
}
|
||||
|
||||
if (failCount > 0) {
|
||||
console.log(`❌ Failed to build ${failCount} package(s)`);
|
||||
if (totalFail > 0) {
|
||||
console.log(`❌ Failed to build ${totalFail} package(s)`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('🎯 Build optimization complete!');
|
||||
}
|
||||
|
||||
// Handle CLI arguments
|
||||
if (process.argv.includes('--help')) {
|
||||
console.log(`
|
||||
Usage: node scripts/build-optimized.js [options]
|
||||
|
||||
Options:
|
||||
--force Force rebuild all packages regardless of changes
|
||||
--verbose Show detailed output for all packages
|
||||
--help Show this help message
|
||||
--force Force rebuild all packages regardless of changes
|
||||
--verbose Show detailed output for all packages
|
||||
--concurrency=N Max parallel builds per level (default: CPU count)
|
||||
--help Show this help message
|
||||
|
||||
Examples:
|
||||
node scripts/build-optimized.js # Build only changed packages
|
||||
node scripts/build-optimized.js --force # Force rebuild all packages
|
||||
node scripts/build-optimized.js --verbose # Show detailed output
|
||||
node scripts/build-optimized.js # Build only changed packages
|
||||
node scripts/build-optimized.js --force # Force rebuild all
|
||||
node scripts/build-optimized.js --concurrency=4 # Limit to 4 parallel
|
||||
node scripts/build-optimized.js --verbose # Show per-package details
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user