Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/code-infra/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ This is stored in the `docs` top-level directory.

### Adding and publishing new packages

Whenever news packages are added to the repo (that will get published to npm) or a private package is turned into a public one, follow the below steps before invoking the publish workflow of the previous section.
Whenever new packages are added to the repo (that will get published to npm) or a private package is turned into a public one, follow the below steps before invoking the publish workflow of the previous section.

1. Goto your repo's code base on your system, open terminal and run:

Expand Down
11 changes: 9 additions & 2 deletions packages/code-infra/src/cli/cmdListWorkspaces.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getWorkspacePackages } from '../utils/pnpm.mjs';
* @property {boolean} [publicOnly] - Whether to filter to only public packages
* @property {'json'|'path'|'name'|'publish-dir'} [output] - Output format (name, path, or json)
* @property {string} [sinceRef] - Git reference to filter changes since
* @property {string[]} [filter] - Same as filtering packages with --filter in pnpm. Only include packages matching the filter. See https://pnpm.io/filtering.
*/

export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
Expand All @@ -37,13 +38,19 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
.option('since-ref', {
type: 'string',
description: 'Filter packages changed since git reference',
})
.option('filter', {
type: 'string',
array: true,
description:
'Same as filtering packages with --filter in pnpm. Only include packages matching the filter. See https://pnpm.io/filtering.',
});
},
handler: async (argv) => {
const { publicOnly = false, output = 'name', sinceRef } = argv;
const { publicOnly = false, output = 'name', sinceRef, filter = [] } = argv;

// Get packages using our helper function
const packages = await getWorkspacePackages({ sinceRef, publicOnly });
const packages = await getWorkspacePackages({ sinceRef, publicOnly, filter });

if (output === 'json') {
// Serialize packages to JSON
Expand Down
92 changes: 4 additions & 88 deletions packages/code-infra/src/cli/cmdNetlifyIgnore.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,93 +12,7 @@ import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { findWorkspaceDir } from '@pnpm/find-workspace-dir';
import { toPosixPath } from '../utils/path.mjs';
import { getWorkspacePackages } from '../utils/pnpm.mjs';

/**
* Get all workspace dependencies (direct and transitive) from a package
* @param {string} packageName - Package name
* @param {Map<string, string>} workspaceMap - Map of workspace name to path
* @param {Map<string, Promise<Set<string>>>} cache - Cache of package resolution promises
* @returns {Promise<Set<string>>} Set of workspace package names (dependencies only, not including the package itself)
*/
async function getWorkspaceDependenciesRecursive(packageName, workspaceMap, cache) {
// Check cache first
const cached = cache.get(packageName);
if (cached) {
return cached;
}

// Create the resolution promise
const promise = (async () => {
const packagePath = workspaceMap.get(packageName);
if (!packagePath) {
throw new Error(`Workspace "${packageName}" not found in the repository`);
}

const packageJsonPath = path.join(packagePath, 'package.json');
const content = await fs.readFile(packageJsonPath, 'utf8');
const packageJson = JSON.parse(content);

// Collect all dependency names
/** @type {Set<string>} */
const allDeps = new Set();
if (packageJson.dependencies) {
Object.keys(packageJson.dependencies).forEach((dep) => allDeps.add(dep));
}
if (packageJson.devDependencies) {
Object.keys(packageJson.devDependencies).forEach((dep) => allDeps.add(dep));
}
if (packageJson.peerDependencies) {
Object.keys(packageJson.peerDependencies).forEach((dep) => allDeps.add(dep));
}

// Filter to only workspace dependencies
const workspaceDeps = Array.from(allDeps).filter((dep) => workspaceMap.has(dep));

// Recursively process workspace dependencies in parallel
const recursiveResults = await Promise.all(
workspaceDeps.map(async (dep) => {
return getWorkspaceDependenciesRecursive(dep, workspaceMap, cache);
}),
);

// Merge all results using flatMap
return new Set(recursiveResults.flatMap((result) => Array.from(result)).concat(workspaceDeps));
})();

// Store in cache before returning
cache.set(packageName, promise);

return promise;
}

/**
* Get transitive workspace dependencies for a list of workspace names
* @param {string[]} workspaceNames - Array of workspace names
* @param {Map<string, string>} workspaceMap - Map of workspace name to path
* @returns {Promise<Set<string>>} Set of workspace package names (including requested packages and all their dependencies)
*/
async function getTransitiveDependencies(workspaceNames, workspaceMap) {
// Shared cache for all workspace dependency resolution
const cache = new Map();

// Validate all workspace names exist
for (const workspaceName of workspaceNames) {
if (!workspaceMap.has(workspaceName)) {
throw new Error(`Workspace "${workspaceName}" not found in the repository`);
}
}

// Process each requested workspace in parallel
const workspaceResults = await Promise.all(
workspaceNames.map((workspaceName) =>
getWorkspaceDependenciesRecursive(workspaceName, workspaceMap, cache),
),
);

// Merge all results using flatMap and add the original package names
return new Set(workspaceNames.concat(workspaceResults.flatMap((result) => Array.from(result))));
}
import { getTransitiveDependencies, getWorkspacePackages } from '../utils/pnpm.mjs';

/**
* Generate the ignore command string for netlify.toml
Expand Down Expand Up @@ -220,7 +134,9 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
console.log(`Processing ${workspaceName}...`);

// Get transitive dependencies for this specific workspace
const dependencyNames = await getTransitiveDependencies([workspaceName], workspaceMap);
const dependencyNames = await getTransitiveDependencies([workspaceName], {
workspacePathByName: workspaceMap,
});

// Convert package names to relative paths (normalize to POSIX separators for git)
const relativePaths = Array.from(dependencyNames)
Expand Down
35 changes: 31 additions & 4 deletions packages/code-infra/src/cli/cmdPublish.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import * as fs from 'node:fs/promises';
import * as semver from 'semver';

import { persistentAuthStrategy } from '../utils/github.mjs';
import { getWorkspacePackages, publishPackages } from '../utils/pnpm.mjs';
import {
getWorkspacePackages,
publishPackages,
validatePublishDependencies,
} from '../utils/pnpm.mjs';
import { getCurrentGitSha, getRepositoryInfo } from '../utils/git.mjs';

const isCI = envCI().isCi;
Expand All @@ -33,6 +37,7 @@ function getOctokit() {
* @property {string} tag NPM dist tag to publish to
* @property {boolean} ci Runs in CI environment
* @property {string} [sha] Git SHA to use for the GitHub release workflow (local only)
* @property {string[]} [filter] Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.
*/

/**
Expand Down Expand Up @@ -260,10 +265,16 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
.option('sha', {
type: 'string',
description: 'Git SHA to use for the GitHub release workflow (local only)',
})
.option('filter', {
type: 'string',
array: true,
description:
'Same as filtering packages with --filter in pnpm. Only publish packages matching the filter. See https://pnpm.io/filtering.',
});
},
handler: async (argv) => {
const { dryRun = false, githubRelease = false, tag = 'latest', sha } = argv;
const { dryRun = false, githubRelease = false, tag = 'latest', sha, filter = [] } = argv;

if (isCI && !argv.ci) {
console.error(
Expand All @@ -290,13 +301,29 @@ export default /** @type {import('yargs').CommandModule<{}, Args>} */ ({
// Get all packages
console.log('🔍 Discovering all workspace packages...');

const allPackages = await getWorkspacePackages({ publicOnly: true });
const allPackages = await getWorkspacePackages({ publicOnly: true, filter });

if (allPackages.length === 0) {
console.log('⚠️ No public packages found in workspace');
console.log(
`⚠️ No publishable packages found in workspace${filter.length > 0 ? ` matching filter "${filter.join(', ')}"` : ''}`,
);
return;
}

if (filter.length > 0) {
console.log('🔍 Validating workspace dependencies for filtered packages...');

const { issues } = await validatePublishDependencies(allPackages);

if (issues.length > 0) {
throw new Error('Invalid dependencies structure of packages to be published.', {
cause: issues,
});
}

console.log('✅ All workspace dependency requirements satisfied');
}

// Get version from root package.json
const version = await getReleaseVersion();

Expand Down
Loading
Loading