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
5 changes: 5 additions & 0 deletions .bumpy/include-extra-packages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'fledgling': minor
---

New `include` config option: list exact package names that have no package.json in the workspace — e.g. per-platform native binary packages published as optional dependencies — and fledgling treats them like discovered packages (claimed, trusted, synced, tab-completed). They're npm-only; `fledgling jsr` skips them.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,28 @@ internal-but-published things you never want it to claim or manage trust for —
Ignored packages are invisible to fledgling: they're left out of `add`, `sync`, `"*"`
globs, and tab completion.

### Extra packages (native binaries, etc.)

Some packages are published from a repo without having their own `package.json` in the
workspace — the classic case is per-platform native binary packages shipped as
`optionalDependencies` and generated at build time. Add their **exact names** (no globs)
to an `"include"` list and fledgling treats them like any discovered package — claimed,
trusted, synced, and tab-completed:

```jsonc
{
"fledgling": {
"include": [
"@scope/my-tool-darwin-arm64",
"@scope/my-tool-linux-x64-gnu"
]
}
}
```

Since these have no manifest of their own, their placeholder claims carry no metadata
beyond the name. They're npm-only: `fledgling jsr` skips them.

### Defaults

| Option | Default | Notes |
Expand Down
6 changes: 3 additions & 3 deletions src/commands/add.command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pc from 'picocolors';
import { findWorkspaceRoot, discoverPackages, detectRepo } from '../workspace.js';
import { findWorkspaceRoot, detectRepo } from '../workspace.js';
import { npmAuthCheck, checkNpmVersion } from '../npm.js';
import { resolveTargets, processTarget, summarize, validateTrustSettings, buildSettings, applyIgnore, type Reporter } from '../core.js';
import { resolveTargets, processTarget, summarize, validateTrustSettings, buildSettings, collectPackages, type Reporter } from '../core.js';
import { loadConfig } from '../config.js';
import { twoFactorDisabledWarning } from '../ui.js';
import { runWizard } from '../interactive.js';
Expand All @@ -11,7 +11,7 @@ import { npmArgs, selectorsOf, type Ctx } from '../args.js';
function runPlain(values: Record<string, any>, selectors: string[]): number {
const root = findWorkspaceRoot();
const config = loadConfig(root);
const discovered = applyIgnore(discoverPackages(root), config.ignore);
const discovered = collectPackages(root, config);
const repo = values.repo ?? detectRepo(root)?.slug;

const resolved = resolveTargets(discovered, selectors, !!values.new, root);
Expand Down
2 changes: 2 additions & 0 deletions src/commands/jsr.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ export async function runJsr(values: Record<string, any>, selectors: string[]):

const spin = hatchSpinner();
spin.start('Scanning workspace');
// `config.include` extras are npm-only: with no package.json in the workspace there's
// nothing to scaffold a jsr.json next to (and native binaries don't belong on JSR).
const discovered = applyIgnore(discoverPackages(root), config.ignore);
const repoInfo = detectRepo(root);
spin.stop(`Found ${pc.bold(String(discovered.length))} package(s)${repoInfo ? ` · ${pc.dim(repoInfo.slug)}` : ''}`);
Expand Down
6 changes: 3 additions & 3 deletions src/commands/sync.command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as p from '@clack/prompts';
import pc from 'picocolors';
import { setTimeout as sleep } from 'node:timers/promises';
import { findWorkspaceRoot, discoverPackages, detectRepo, type Pkg } from '../workspace.js';
import { findWorkspaceRoot, detectRepo, type Pkg } from '../workspace.js';
import { npmAuthCheck, checkNpmVersion, listTrust, configureTrust, revokeTrust, warmNpmAuth, publishedNames } from '../npm.js';
import { npmArgs, selectorsOf, type Ctx } from '../args.js';
import {
Expand All @@ -12,7 +12,7 @@ import {
trustMatches,
describeTrustDiff,
describeConfig,
applyIgnore,
collectPackages,
trustReadHint,
} from '../core.js';
import { loadConfig } from '../config.js';
Expand Down Expand Up @@ -42,7 +42,7 @@ export async function runSync(values: Record<string, any>, selectors: string[]):
// Reports "logged in as…" and warns if 2FA is off (trust writes would 403).
reportNpmAuth(auth);

const discovered = applyIgnore(discoverPackages(root), config.ignore);
const discovered = collectPackages(root, config);

const resolved = resolveTargets(discovered, selectors, false, root);
if (resolved.error) {
Expand Down
6 changes: 6 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export interface FledglingConfig {
trust?: boolean;
/** Package names/globs to exclude from fledgling entirely (besides `"private": true`). */
ignore?: string[];
/**
* Extra package names to manage that have no package.json in the workspace —
* e.g. per-platform native binary packages published as optional dependencies.
* Exact names (no globs), treated like discovered packages everywhere but JSR.
*/
include?: string[];
provider?: Provider;
/**
* May the trusted publisher run `npm publish` directly? (default: true). npm always
Expand Down
17 changes: 16 additions & 1 deletion src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,25 @@ export function applyIgnore(pkgs: Pkg[], ignore?: string[]): Pkg[] {
return pkgs.filter(p => !res.some(re => re.test(p.name)));
}

/**
* The full package list: workspace discovery, minus `ignore`, plus `include` — extra
* names with no package.json of their own (e.g. per-platform native binary packages
* shipped as optional deps). Extras are synthesized at the workspace root with a bare
* manifest; being explicit, they are not subject to `ignore`.
*/
export function collectPackages(root: string, config: FledglingConfig): Pkg[] {
const pkgs = applyIgnore(discoverPackages(root), config.ignore);
const seen = new Set(pkgs.map(p => p.name));
for (const name of config.include ?? []) {
if (!seen.has(name)) (seen.add(name), pkgs.push({ name, dir: root, manifest: { name } }));
}
return pkgs;
}

/** Package names in the current workspace — used for tab completion and the wizard. */
export function workspacePackages(): Pkg[] {
const root = findWorkspaceRoot();
return applyIgnore(discoverPackages(root), loadConfig(root).ignore);
return collectPackages(root, loadConfig(root));
}

export interface ResolveResult {
Expand Down
6 changes: 3 additions & 3 deletions src/interactive.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as p from '@clack/prompts';
import pc from 'picocolors';
import { findWorkspaceRoot, discoverPackages, detectRepo, type Pkg } from './workspace.js';
import { findWorkspaceRoot, detectRepo, type Pkg } from './workspace.js';
import { npmAuthCheck, publishedNames, warmNpmAuth, validatePackageName, isNameAvailable } from './npm.js';
import {
resolveTargets,
processTarget,
summarize,
describeConfig,
applyIgnore,
collectPackages,
type Settings,
type Reporter,
type TargetResult,
Expand All @@ -32,7 +32,7 @@ export async function runWizard(values: Record<string, any>, selectors: string[]
const newClaim = !!values.new;
const spin = hatchSpinner();
if (!newClaim) spin.start('Scanning workspace');
const discovered = applyIgnore(discoverPackages(root), config.ignore);
const discovered = collectPackages(root, config);
const repoInfo = detectRepo(root);
if (!newClaim) {
spin.stop(`Found ${pc.bold(String(discovered.length))} package(s)${repoInfo ? ` · ${pc.dim(repoInfo.slug)}` : ''}`);
Expand Down