Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/core/src/cli/commands/init.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { input, checkbox, confirm } from '@inquirer/prompts';
import { tryCatch } from '@dmno/ts-lib';
import Debug from 'debug';

import { findDmnoServices } from '../../config-loader/find-services';
import { findDmnoServices, selectAndApplyWorkspaceConfig } from '../../config-loader/find-services';
import { DmnoCommand } from '../lib/dmno-command';

import { joinAndCompact } from '../lib/formatting';
Expand Down Expand Up @@ -33,11 +33,13 @@ program.action(async (opts: {
console.log(DMNO_DEV_BANNER);
// console.log(kleur.gray('let us help you connect the dots ● ● ●'));

const [workspaceInfo] = await Promise.all([
const [workspaceInfoInitial] = await Promise.all([
findDmnoServices(true),
fallingDmnosAnimation(),
]);

// Handle workspace configuration selection after animation completes
const workspaceInfo = await selectAndApplyWorkspaceConfig(workspaceInfoInitial);

// await fallingDmnoLoader('● Scanning repo', '● Scan complete');

Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/config-loader/config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ViteNodeServer } from 'vite-node/server';
import { createDeferredPromise } from '@dmno/ts-lib';
import { createDebugTimer } from '../cli/lib/debug-timer';
import { setupViteServer } from './vite-server';
import { ScannedWorkspaceInfo, findDmnoServices } from './find-services';
import { ScannedWorkspaceInfo, findDmnoServices, selectAndApplyWorkspaceConfig } from './find-services';
import {
DmnoService, DmnoWorkspace, DmnoServiceConfig,
} from '../config-engine/config-engine';
Expand Down Expand Up @@ -55,7 +55,8 @@ export class ConfigLoader {
}

private async finishInit() {
this.workspaceInfo = await findDmnoServices();
const workspaceInfoInitial = await findDmnoServices();
this.workspaceInfo = await selectAndApplyWorkspaceConfig(workspaceInfoInitial);
// already filtered to only services with a .dmno folder
const dmnoServicePackages = this.workspaceInfo.workspacePackages;

Expand Down
219 changes: 214 additions & 5 deletions packages/core/src/config-loader/find-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import readYamlFile from 'read-yaml-file';
import { fdir } from 'fdir';
import { tryCatch } from '@dmno/ts-lib';
import Debug from 'debug';
import { checkbox } from '@inquirer/prompts';
import { pathExists } from '../lib/fs-utils';


Expand Down Expand Up @@ -41,6 +42,7 @@ export type ScannedWorkspaceInfo = {
workspacePackages: Array<WorkspacePackagesListing>,
autoSelectedPackage?: WorkspacePackagesListing,
settings?: WorkspaceSettings,
workspaceConfigs?: WorkspaceConfigSource[],
};

// list of locations to look for workspace project globs
Expand All @@ -59,6 +61,13 @@ const WORKSPACE_SETTINGS_LOCATIONS = [
// { file: 'deno.jsonc', path: 'workspace', optional: true },
];

export type WorkspaceConfigSource = {
filePath: string;
settingsLocation: typeof WORKSPACE_SETTINGS_LOCATIONS[number];
packagePatterns: string[];
otherSettings?: WorkspaceSettings;
};

export async function findDmnoServices(includeUnitialized = false): Promise<ScannedWorkspaceInfo> {
const startAt = new Date();

Expand All @@ -67,6 +76,7 @@ export async function findDmnoServices(includeUnitialized = false): Promise<Scan
let dmnoWorkspaceFinal = false;
let isMonorepo = false;
let packagePatterns: Array<string> | undefined;
let workspaceConfigs: WorkspaceConfigSource[] = [];

let otherSettings: WorkspaceSettings | undefined;

Expand All @@ -82,9 +92,12 @@ export async function findDmnoServices(includeUnitialized = false): Promise<Scan
const filePath = path.join(cwd, settingsLocation.file);
if (!await pathExists(filePath)) continue;

debug(`found workspace settings file: ${filePath}`);

// assume first package.json file found is root until we find evidence otherwise
if (settingsLocation.file === 'package.json' && !dmnoWorkspaceRootPath) {
dmnoWorkspaceRootPath = cwd;
debug(`set dmnoWorkspaceRootPath to ${cwd} (from package.json)`);
}

debug(`looking for workspace globs in ${filePath} > ${settingsLocation.globsPath}`);
Expand All @@ -97,16 +110,38 @@ export async function findDmnoServices(includeUnitialized = false): Promise<Scan
}
const possiblePackagePatterns = _.get(fileContents, settingsLocation.globsPath);
if (possiblePackagePatterns) {
packagePatterns = possiblePackagePatterns;
dmnoWorkspaceRootPath = cwd;
isMonorepo = true;
// Handle both legacy format (workspaces as array) and modern format (workspaces as object with packages property)
let actualPackagePatterns: string[] | undefined;
if (Array.isArray(possiblePackagePatterns)) {
actualPackagePatterns = possiblePackagePatterns;
} else if (possiblePackagePatterns && typeof possiblePackagePatterns === 'object' && possiblePackagePatterns.packages) {
actualPackagePatterns = possiblePackagePatterns.packages;
}

if (actualPackagePatterns) {
debug(`found workspace patterns in ${filePath}:`, actualPackagePatterns);

// Store this workspace configuration
workspaceConfigs.push({
filePath,
settingsLocation,
packagePatterns: actualPackagePatterns,
otherSettings: settingsLocation.file === '.dmno/workspace.yaml' ? _.omit(fileContents, settingsLocation.globsPath) : undefined
});

// Set workspace root if not already set
if (!dmnoWorkspaceRootPath) {
dmnoWorkspaceRootPath = cwd;
debug(`updated dmnoWorkspaceRootPath to ${cwd} (from workspace patterns)`);
}
isMonorepo = true;
}
}

// if this is our dmno-specific file, we'll consider this "final" and stop scanning upwards
if (settingsLocation.file === '.dmno/workspace.yaml') {
debug(`found dmno workspace config at ${filePath} - stopping upward scan`);
dmnoWorkspaceFinal = true;
// everything else in the yaml file is additional settings
otherSettings = _.omit(fileContents, settingsLocation.globsPath);
break; // breaks from for loop - will still continue looking for git root
}
}
Expand All @@ -131,6 +166,24 @@ export async function findDmnoServices(includeUnitialized = false): Promise<Scan
throw new Error('Unable to detect dmno workspace root');
}

// Process workspace configurations
if (workspaceConfigs.length > 0) {
// If there's only one config or they're all equivalent, select immediately
if (workspaceConfigs.length === 1 || areWorkspaceConfigsEquivalent(workspaceConfigs)) {
const selectedConfig = await selectWorkspaceConfig(workspaceConfigs);
packagePatterns = selectedConfig.packagePatterns;
if (selectedConfig.otherSettings) {
otherSettings = selectedConfig.otherSettings;
}
} else {
// Return the configs to be selected by the calling code
// Use the first config temporarily for the scanning phase
packagePatterns = workspaceConfigs[0].packagePatterns;
if (workspaceConfigs[0].otherSettings) {
otherSettings = workspaceConfigs[0].otherSettings;
}
}
}

// const { packageManager, rootWorkspacePath: rootServicePath } = await detectPackageManager();

Expand Down Expand Up @@ -205,5 +258,161 @@ export async function findDmnoServices(includeUnitialized = false): Promise<Scan
workspacePackages: includeUnitialized ? workspacePackages : _.filter(workspacePackages, (p) => p.dmnoFolder),
autoSelectedPackage: packageFromPwd || packageFromCurrentPackageName,
settings: otherSettings,
workspaceConfigs: workspaceConfigs.length > 1 && !areWorkspaceConfigsEquivalent(workspaceConfigs) ? workspaceConfigs : undefined,
};
}

function standardizePackagePatterns(patterns: string[]): string[] {
return _.uniq(patterns.map(p => p.trim()).filter(Boolean)).sort();
}

function areWorkspaceConfigsEquivalent(configs: WorkspaceConfigSource[]): boolean {
if (configs.length <= 1) return true;

const standardizedPatterns = configs.map(config =>
standardizePackagePatterns(config.packagePatterns)
);

const firstPatterns = standardizedPatterns[0];
return standardizedPatterns.every(patterns =>
_.isEqual(patterns, firstPatterns)
);
}

async function selectWorkspaceConfig(configs: WorkspaceConfigSource[]): Promise<{ packagePatterns: string[], otherSettings?: WorkspaceSettings }> {
if (configs.length === 0) {
throw new Error('No workspace configurations found');
}

if (configs.length === 1) {
return {
packagePatterns: configs[0].packagePatterns,
otherSettings: configs[0].otherSettings
};
}

// Check if all configurations are equivalent
if (areWorkspaceConfigsEquivalent(configs)) {
debug(`Found ${configs.length} workspace configurations, but they are equivalent - proceeding with first one`);
return {
packagePatterns: configs[0].packagePatterns,
otherSettings: configs[0].otherSettings
};
}

// Check if we're in a non-interactive environment
const isNonInteractive = process.env.CI || process.env.TERM === 'dumb' || !process.stdin.isTTY;

if (isNonInteractive) {
debug(`Found ${configs.length} different workspace configurations, using first one in non-interactive mode`);
debug(`Selected: ${configs[0].filePath} - ${configs[0].packagePatterns.join(', ')}`);
return {
packagePatterns: configs[0].packagePatterns,
otherSettings: configs[0].otherSettings
};
}

// Show user selection for different configurations
const selectedConfigIndices = await checkbox({
message: `Found ${configs.length} different workspace configurations.\nSelect which to use (Press <space> to select, <a> to toggle all, <enter> to proceed):\n`,
choices: configs.map((config, index) => ({
name: `${config.filePath} - [${config.packagePatterns.join(', ')}]`,
value: index,
checked: index === 0 // Default to first one
})),
instructions: false
});

if (selectedConfigIndices.length === 0) {
throw new Error('No workspace configuration selected');
}

// Combine selected configurations and deduplicate
const selectedConfigs = selectedConfigIndices.map(index => configs[index]);
const allPatterns = _.flatten(selectedConfigs.map(config => config.packagePatterns));
const deduplicatedPatterns = standardizePackagePatterns(allPatterns);

// Merge other settings from selected configs
const mergedOtherSettings = selectedConfigs.reduce((acc, config) => {
if (config.otherSettings) {
return _.merge(acc, config.otherSettings);
}
return acc;
}, {} as WorkspaceSettings);

debug(`Selected ${selectedConfigs.length} workspace configurations, deduplicated to ${deduplicatedPatterns.length} patterns`);

return {
packagePatterns: deduplicatedPatterns,
otherSettings: Object.keys(mergedOtherSettings).length > 0 ? mergedOtherSettings : undefined
};
}

export async function selectAndApplyWorkspaceConfig(workspaceInfo: ScannedWorkspaceInfo): Promise<ScannedWorkspaceInfo> {
if (!workspaceInfo.workspaceConfigs || workspaceInfo.workspaceConfigs.length <= 1) {
return workspaceInfo;
}

const selectedConfig = await selectWorkspaceConfig(workspaceInfo.workspaceConfigs);

// Get the workspace root path from the original info
const dmnoWorkspaceRootPath = workspaceInfo.workspacePackages.find(p => p.isRoot)?.path;
if (!dmnoWorkspaceRootPath) {
throw new Error('Unable to find workspace root path');
}

// Rebuild package paths with selected patterns
let packagePaths = [dmnoWorkspaceRootPath];
if (workspaceInfo.isMonorepo && selectedConfig.packagePatterns?.length) {
const fullPackagePatterns = selectedConfig.packagePatterns.map((gi) => path.join(dmnoWorkspaceRootPath, gi));
const patternsByType = _.groupBy(
fullPackagePatterns,
(s) => (s.includes('*') ? 'globs' : 'dirs'),
);

const expandedPathsFromGlobs = await (
new fdir()
.withBasePath()
.onlyDirs()
.glob(...patternsByType.globs || [])
.exclude((dirName, _dirPath) => {
return dirName === 'node_modules';
})
.crawl(dmnoWorkspaceRootPath)
.withPromise()
);
packagePaths.push(...patternsByType.dirs || []);
packagePaths.push(...expandedPathsFromGlobs);
packagePaths = packagePaths.map((p) => p.replace(/\/$/, ''));
packagePaths = _.uniq(packagePaths);
}

// Rebuild workspace packages
const workspacePackages = _.compact(await Promise.all(packagePaths.map(async (packagePath) => {
const packageJson = await tryCatch(
async () => await readJsonFile(path.join(packagePath, 'package.json')),
(err) => {
if ((err as any).code === 'ENOENT') return undefined;
throw err;
},
);

const dmnoFolderExists = await pathExists(path.join(packagePath, '.dmno'));
const packageName = packageJson?.name || packagePath.split('/').pop();

return {
isRoot: packagePath === dmnoWorkspaceRootPath,
path: packagePath,
relativePath: packagePath.substring(dmnoWorkspaceRootPath.length + 1),
name: packageName,
dmnoFolder: dmnoFolderExists,
};
})));

return {
...workspaceInfo,
workspacePackages,
settings: selectedConfig.otherSettings || workspaceInfo.settings,
workspaceConfigs: undefined // Clear this since we've made the selection
};
}
Loading