Skip to content
Draft
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
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,47 @@ The first `--` token is the delimiter. It is omitted from the returned array, an

`createPositionalArguments()` does not mutate the input array.

### inspectFlags(flagSchema, options)

Inspects the flag names that `typeFlag()` accepts for a schema. This is mainly useful for parser or framework integrations that need to avoid duplicating type-flag's flag recognition rules for conflict checks, suggestions, or help metadata.

Type:

```ts
const inspectFlags: (
flagSchema: Flags,
options?: { booleanNegation?: boolean }
) => InspectedFlag[]

type InspectedFlag = {
name: string
kebabName: string
longNames: string[]
alias?: string
tokens: string[]
negatedTokens: string[]
isArray: boolean
isBoolean: boolean
}
```

Example:

```ts
const inspected = inspectFlags({
getID: String,
verbose: {
type: Boolean,
alias: 'v'
}
}, { booleanNegation: true })

inspected[0].tokens // ['--getID', '--get-id']
inspected[1].tokens // ['--verbose', '-v', '--no-verbose', '--no-v']
```

`inspectFlags()` validates the schema with the same flag-name and alias rules used by `typeFlag()`.

## Comparison With Other Parsers

Choose _type-flag_ when you want a tiny parser whose schema returns the values your app actually uses.
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ export { typeFlag } from './type-flag.ts';
export { getFlag } from './get-flag.ts';
export { flagNameToKebab } from './utils.ts';
export { createPositionalArguments } from './positional-arguments.ts';
export { inspectFlags } from './inspect-flags.ts';
export type {
TypeFlag,
TypeFlagOptions,
Flags,
IgnoreFunction,
PositionalArguments,
InspectFlagsOptions,
InspectedFlag,
} from './types.ts';
124 changes: 124 additions & 0 deletions src/inspect-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type {
Flags,
InspectedFlag,
InspectFlagsOptions,
} from './types.ts';
import {
flagNameToKebab,
getFlagAlias,
getFlagLongNames,
hasOwn,
parseFlagType,
setFlag,
} from './utils.ts';

type FlagInspectionData = {
alias?: string;
flag: InspectedFlag;
parser: unknown;
registryNames: string[];
};

const getNormalTokens = (
flagName: string,
longNames: string[],
alias?: string,
) => [
...longNames.map(longName => `--${longName}`),
...(flagName.length === 1 ? [`-${flagName}`] : []),
...(alias ? [`-${alias}`] : []),
];

const inspectFlag = (
registry: Record<string, string>,
schemas: Flags,
flagName: string,
): FlagInspectionData | undefined => {
if (!hasOwn(schemas, flagName)) {
return;
}

const schema = schemas[flagName];
const longNames = getFlagLongNames(flagName);
const alias = getFlagAlias(flagName, schema);
const [parser, isArray] = parseFlagType(schema);

for (const longName of longNames) {
setFlag(registry, longName, flagName);
}

if (alias) {
setFlag(registry, alias, flagName);
}

const acceptedLongNames = flagName.length === 1 ? [] : longNames;

return {
alias,
parser,
registryNames: [
...longNames,
...(alias ? [alias] : []),
],
flag: {
name: flagName,
kebabName: flagNameToKebab(flagName),
longNames: acceptedLongNames,
...(alias ? { alias } : {}),
tokens: getNormalTokens(
flagName,
acceptedLongNames,
alias,
),
negatedTokens: [],
isArray,
isBoolean: parser === Boolean,
},
};
};

const applyBooleanNegation = (
registry: Record<string, string>,
flags: FlagInspectionData[],
) => {
for (const { flag, parser, registryNames } of flags) {
if (parser !== Boolean) {
continue;
}

const negatedTokens = registryNames
.map(flagName => `no-${flagName}`)
.filter(negatedFlagName => !hasOwn(registry, negatedFlagName))
.map(negatedFlagName => `--${negatedFlagName}`);

flag.negatedTokens = negatedTokens;
flag.tokens = [
...flag.tokens,
...negatedTokens,
];
}
};

export const inspectFlags = (
schemas: Flags,
{ booleanNegation }: InspectFlagsOptions = {},
): InspectedFlag[] => {
const registry: Record<string, string> = {};
const flags: FlagInspectionData[] = [];
for (const flagName in schemas) {
if (!hasOwn(schemas, flagName)) {
continue;
}

const flag = inspectFlag(registry, schemas, flagName);
if (flag) {
flags.push(flag);
}
}

if (booleanNegation) {
applyBooleanNegation(registry, flags);
}

return flags.map(({ flag }) => flag);
};
41 changes: 41 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,47 @@ export type PositionalArguments = string[] & {
'--': string[];
};

/**
* Options for inspecting the accepted flag surface for a schema.
*/
export type InspectFlagsOptions = {

/**
* Include `--no-<flag>` tokens for boolean flags when they would be parsed as negation.
*/
booleanNegation?: boolean;
};

/**
* Public metadata describing one schema flag's accepted argv surface.
*/
export type InspectedFlag = {

/** The original schema key and parsed output key. */
name: string;

/** The kebab-case long-form name derived from the schema key. */
kebabName: string;

/** Long-form names accepted after `--`, without the `--` prefix. */
longNames: string[];

/** Single-character short alias accepted after `-`, if one is configured. */
alias?: string;

/** Accepted argv tokens, including prefixes and boolean negation tokens when enabled. */
tokens: string[];

/** Accepted boolean-negation argv tokens, including the `--` prefix. */
negatedTokens: string[];

/** Whether this flag collects multiple values. */
isArray: boolean;

/** Whether this flag uses `Boolean` as its parser. */
isBoolean: boolean;
};

// Infers the type from the default value of a flag schema.
// Note: Preserves literal types from default functions (e.g., () => 'hello' infers 'hello').
// Users can widen types explicitly with type assertions (e.g., 'hello' as string).
Expand Down
72 changes: 47 additions & 25 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,46 @@ const validateFlagName = (
}
};

export const getFlagLongNames = (
flagName: string,
) => {
validateFlagName(flagName);

const kebabName = flagNameToKebab(flagName);

return (
flagName === kebabName
? [flagName]
: [flagName, kebabName]
);
};

export const getFlagAlias = (
flagName: string,
schema: FlagTypeOrSchema,
) => {
if (!('alias' in schema) || typeof schema.alias !== 'string') {
return;
}

const { alias } = schema;
const errorPrefix = `Flag alias "${alias}" for flag "${flagName}"`;

if (flagName.length === 1) {
throw new Error(`${errorPrefix} cannot be defined for a single-character flag`);
}

if (alias.length === 0) {
throw new Error(`${errorPrefix} cannot be empty`);
}

if (alias.length > 1) {
throw new Error(`${errorPrefix} must be a single character`);
}

return alias;
};

type FlagParsingData = [
values: unknown[],
parser: TypeFunction,
Expand All @@ -109,10 +149,10 @@ type FlagRegistry = {
[flagName: string]: FlagParsingData;
};

const setFlag = (
registry: FlagRegistry,
export const setFlag = <FlagData>(
registry: Record<string, FlagData>,
flagName: string,
data: FlagParsingData,
data: FlagData,
) => {
if (hasOwn(registry, flagName)) {
throw new Error(`Duplicate flags named "${flagName}"`);
Expand All @@ -130,7 +170,6 @@ export const createRegistry = (
if (!hasOwn(schemas, flagName)) {
continue;
}
validateFlagName(flagName);

const schema = schemas[flagName];
const flagData: FlagParsingData = [
Expand All @@ -139,29 +178,12 @@ export const createRegistry = (
schema,
];

setFlag(registry, flagName, flagData);

const kebabCasing = flagNameToKebab(flagName);
if (flagName !== kebabCasing) {
setFlag(registry, kebabCasing, flagData);
for (const longName of getFlagLongNames(flagName)) {
setFlag(registry, longName, flagData);
}

if ('alias' in schema && typeof schema.alias === 'string') {
const { alias } = schema;
const errorPrefix = `Flag alias "${alias}" for flag "${flagName}"`;

if (flagName.length === 1) {
throw new Error(`${errorPrefix} cannot be defined for a single-character flag`);
}

if (alias.length === 0) {
throw new Error(`${errorPrefix} cannot be empty`);
}

if (alias.length > 1) {
throw new Error(`${errorPrefix} must be a single character`);
}

const alias = getFlagAlias(flagName, schema);
if (alias) {
setFlag(registry, alias, flagData);
}
}
Expand Down
1 change: 1 addition & 0 deletions tests/specs/type-flag/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe } from 'manten';

describe('type-flag', () => {
import('./error-handling.ts');
import('./inspection.ts');
import('./types.ts');
import('./parsing.ts');
});
Loading
Loading