Enforce kebab casing for CLI flags
MODIFY
packages/@romejs/cli-flags/Parser.ts
packages/@romejs/cli-flags/Parser.ts
@@ -70,6 +70,8 @@ export default class Parser<T> {
this.opts = opts;
this.shorthandFlags = new Set();
+ this.incorrectCaseFlags = new Set();
+
this.declaredFlags = new Map();
this.defaultFlags = new Map();
this.flags = new Map();
@@ -121,6 +123,7 @@ export default class Parser<T> {
reporter: Reporter;
opts: ParserOptions<T>;
+ incorrectCaseFlags: Set<string>;
shorthandFlags: Set<string>;
flags: Map<string, string | boolean>;
defaultFlags: Map<string, unknown>;
@@ -139,6 +142,17 @@ export default class Parser<T> {
return flag !== undefined && flag[0] === '-';
}
+ toCamelCase(name: string): string {
+ const camelName = toCamelCase(name);
+
+ // Don't allow passing in straight camelcased names
+ if (toKebabCase(name) !== name) {
+ this.incorrectCaseFlags.add(name);
+ }
+
+ return camelName;
+ }
+
consumeRawArgs(rawArgs: Array<string>) {
while (rawArgs.length > 0) {
const arg: string = String(rawArgs.shift());
@@ -153,31 +167,34 @@ export default class Parser<T> {
// Flags beginning with no- are always false
if (name.startsWith('no-')) {
- this.flags.set(name.slice(3), false);
+ const camelName = this.toCamelCase(name.slice(3));
+ this.flags.set(camelName, false);
continue;
}
// Allow for arguments to be passed as --foo=bar
const equalsIndex = name.indexOf('=');
if (equalsIndex !== -1) {
- const cleanName = name.slice(0, equalsIndex);
+ const cleanName = this.toCamelCase(name.slice(0, equalsIndex));
const value = name.slice(equalsIndex + 1);
this.flags.set(cleanName, value);
continue;
}
+ const camelName = this.toCamelCase(name);
+
// If the next argument is a flag or we're at the end of the args then just set it to `true`
if (rawArgs.length === 0 || this.looksLikeFlag(rawArgs[0])) {
- this.flags.set(name, true);
+ this.flags.set(camelName, true);
} else {
// Otherwise, take that value
- this.flags.set(name, String(rawArgs.shift()));
+ this.flags.set(camelName, String(rawArgs.shift()));
}
- this.flagToArgIndex.set(name, this.args.length);
+ this.flagToArgIndex.set(camelName, this.args.length);
if (arg[0] === '-' && arg[1] !== '-') {
- this.shorthandFlags.add(name);
+ this.shorthandFlags.add(camelName);
}
} else {
// Not a flag and hasn't been consumed already by a previous arg so it must be a file
@@ -186,20 +203,6 @@ export default class Parser<T> {
}
}
- setFlagAlias(key: string, alias: string) {
- const value = this.flags.get(key);
- if (value !== undefined) {
- this.flags.delete(key);
- this.flags.set(alias, value);
- }
-
- const argIndex = this.flagToArgIndex.get(key);
- if (argIndex !== undefined) {
- this.flagToArgIndex.set(alias, argIndex);
- this.flagToArgIndex.delete(key);
- }
- }
-
getFlagsConsumer(): Consumer {
const defaultFlags: Dict<unknown> = {};
@@ -227,6 +230,13 @@ export default class Parser<T> {
valueConsumer.setValue('');
}
+ this.declareArgument({
+ name: key,
+ command: this.currentCommand,
+ definition: def,
+ });
+ defaultFlags[key] = def.default;
+
// We've parsed arguments like `--foo bar` as `{foo: 'bar}`
// However, --foo may be a boolean flag, so `bar` needs to be correctly added to args
if (def.type === 'boolean' && value !== true && value !== false &&
@@ -246,20 +256,19 @@ export default class Parser<T> {
//
valueConsumer.setValue(true);
}
-
- this.declareArgument({
- name: key,
- command: this.currentCommand,
- definition: def,
- });
- defaultFlags[key] = def.default;
},
context: {
category: 'flags/invalid',
+
+ normalizeKey: (key) => {
+ return this.incorrectCaseFlags.has(key) ? key : toKebabCase(key);
+ },
+
getOriginalValue: (keys: ConsumePath) => {
return flags[keys[0]];
},
+
getDiagnosticPointer: (
keys: ConsumePath,
target: ConsumeSourceLocationRequestTarget,
@@ -275,6 +284,7 @@ export default class Parser<T> {
args: this.args,
defaultFlags,
flags,
+ incorrectCaseFlags: this.incorrectCaseFlags,
shorthandFlags: this.shorthandFlags,
}, {
type: 'flag',
@@ -302,7 +312,6 @@ export default class Parser<T> {
}
// Declare argument
- this.setFlagAlias(toKebabCase(key), key);
this.declaredFlags.set(key, decl);
this.defaultFlags.set(key, decl.definition.default);
}
@@ -339,13 +348,29 @@ export default class Parser<T> {
let definedCommand: undefined | DefinedCommand;
- const rootFlags = await consumer.bufferDiagnostics(async (consumer) => {
+ const rootFlags = await consumer.bufferDiagnostics(
+ async (consumer) => {
for (const shorthandName of this.shorthandFlags) {
consumer.get(shorthandName).unexpected(
`Shorthand flags are not supported`,
);
}
+ for (const incorrectName of this.incorrectCaseFlags) {
+ consumer.get(incorrectName).unexpected(
+ `Incorrect cased flag name`,
+ {
+ advice: [
+ {
+ type: 'log',
+ category: 'info',
+ message: markup`Use <emphasis>${toKebabCase(incorrectName)}</emphasis> instead`,
+ },
+ ],
+ },
+ );
+ }
+
const rootFlags = this.opts.defineFlags(consumer);
for (const key of this.commands.keys()) {
@@ -363,7 +388,8 @@ export default class Parser<T> {
this.currentCommand = undefined;
return rootFlags;
- });
+ },
+ );
// Show help for --help
if (this.helpMode) {

MODIFY
packages/@romejs/cli-flags/serializeCLIFlags.ts
packages/@romejs/cli-flags/serializeCLIFlags.ts
@@ -16,6 +16,7 @@ type SerializeCLIData = {
args: Array<string>;
defaultFlags: Dict<unknown>;
flags: Dict<unknown>;
+ incorrectCaseFlags: Set<string>;
shorthandFlags: Set<string>;
};
@@ -104,7 +105,7 @@ export function serializeCLIFlags(
}
const flagPrefix = data.shorthandFlags.has(key) ? '-' : '--';
- const kebabKey = toKebabCase(key);
+ const kebabKey = data.incorrectCaseFlags.has(key) ? key : toKebabCase(key);
if (val === false) {
code += `${flagPrefix}no-${kebabKey} `;
} else {

MODIFY
packages/@romejs/consume/Consumer.ts
packages/@romejs/consume/Consumer.ts
@@ -69,35 +69,6 @@ function isComputedPart(part: ConsumeKey): boolean {
return typeof part === 'number' || !isValidIdentifierName(part);
}
-function joinPath(path: ConsumePath): string {
- let str = '';
-
- for (let i = 0; i < path.length; i++) {
- const part = path[i];
- const nextPart = path[i + 1];
-
- // If we are a computed property then wrap in brackets, the previous part would not have inserted a dot
- if (isComputedPart(part)) {
- const inner = typeof part === 'number'
- ? String(part)
- : escapeString(part, {
- quote: "'",
- });
-
- str += `[${inner}]`;
- } else {
- if (nextPart === undefined || isComputedPart(nextPart)) {
- // Don't append a dot if there are no parts or the next is computed
- str += part;
- } else {
- str += `${part}.`;
- }
- }
- }
-
- return str;
-}
-
export default class Consumer {
constructor(opts: ConsumerOptions) {
this.path = opts.filePath;
@@ -290,6 +261,41 @@ export default class Consumer {
return this.getDiagnosticLocation() !== undefined;
}
+ getKeyPathString(path: ConsumePath = this.keyPath): string {
+ const {normalizeKey} = this.context;
+ let str = '';
+
+ for (let i = 0; i < path.length; i++) {
+ let part = path[i];
+ const nextPart = path[i + 1];
+
+ if (typeof part === 'string' && normalizeKey !== undefined) {
+ part = normalizeKey(part);
+ }
+
+ // If we are a computed property then wrap in brackets, the previous part would not have inserted a dot
+ // We allow a computed part at the beginning of a path
+ if (isComputedPart(part) && i > 0) {
+ const inner = typeof part === 'number'
+ ? String(part)
+ : escapeString(part, {
+ quote: "'",
+ });
+
+ str += `[${inner}]`;
+ } else {
+ if (nextPart === undefined || isComputedPart(nextPart)) {
+ // Don't append a dot if there are no parts or the next is computed
+ str += part;
+ } else {
+ str += `${part}.`;
+ }
+ }
+ }
+
+ return str;
+ }
+
generateUnexpectedMessage(
msg: string,
opts: UnexpectedConsumerOptions,
@@ -309,9 +315,9 @@ export default class Consumer {
}
if (at === 'suffix') {
- msg += ` at <emphasis>${joinPath(target.keyPath)}</emphasis>`;
+ msg += ` at <emphasis>${target.getKeyPathString()}</emphasis>`;
} else {
- msg = `<emphasis>${joinPath(target.keyPath)}</emphasis> ${msg}`;
+ msg = `<emphasis>${target.getKeyPathString()}</emphasis> ${msg}`;
}
return msg;
@@ -355,7 +361,6 @@ export default class Consumer {
} while (consumer !== undefined);
// If consumer is undefined and we have no filename then we were not able to find a location,
-
// in this case, just throw a normal error
if (consumer === undefined && filename === undefined) {
throw new Error(msg);
@@ -363,13 +368,13 @@ export default class Consumer {
// Warn that we didn't find this value in the source if it's parent wasn't either
if (this.parent === undefined || !this.parent.wasInSource()) {
- advice.push({
+ advice.push(
+ {
type: 'log',
category: 'warn',
- message: `This value was expected to be found at <emphasis>${joinPath(
- this.keyPath,
- )}</emphasis> but was not in the original source`,
- });
+ message: `This value was expected to be found at <emphasis>${this.getKeyPathString()}</emphasis> but was not in the original source`,
+ },
+ );
}
}
@@ -544,7 +549,9 @@ export default class Consumer {
for (const [key, value] of this.asMap(false, false)) {
if (!this.usedNames.has(key)) {
- value.unexpected(`Unknown <emphasis>${key}</emphasis> ${type}`, {
+ value.unexpected(`Unknown <emphasis>${this.getKeyPathString([
+ key,
+ ])}</emphasis> ${type}`, {
target: 'key',
at: 'suffix',
atParent: true,

MODIFY
packages/@romejs/consume/types.ts
packages/@romejs/consume/types.ts
@@ -28,6 +28,7 @@ export type ConsumeSourceLocationRequestTarget =
export type ConsumeContext = {
category: DiagnosticCategory;
+ normalizeKey?: (key: string) => string;
getDiagnosticPointer?: (
keys: ConsumePath,
target: ConsumeSourceLocationRequestTarget,

MODIFY
packages/@romejs/core/master/Master.ts
packages/@romejs/core/master/Master.ts
@@ -62,6 +62,7 @@ import VirtualModules from './fs/VirtualModules';
import {
DiagnosticsProcessorOptions,
} from '@romejs/diagnostics/DiagnosticsProcessor';
+import {toKebabCase} from '@romejs/string-utils';
const STDOUT_MAX_CHUNK_LENGTH = 100_000;
@@ -767,7 +768,6 @@ export default class Master {
const defaultCommandFlags: Dict<unknown> = {};
// A type-safe wrapper for retrieving command flags
-
// TODO perhaps present this as JSON or something if this isn't a request from the CLI?
const commandFlagsConsumer = consume({
filePath: createUnknownFilePath('argv'),
@@ -786,6 +786,10 @@ export default class Master {
return undefined;
},
+ normalizeKey: (key) => {
+ return toKebabCase(key);
+ },
+
getDiagnosticPointer: (keys: ConsumePath) => {
return req.getDiagnosticPointerFromFlags({
type: 'flag',

MODIFY
packages/@romejs/core/master/MasterRequest.ts
packages/@romejs/core/master/MasterRequest.ts
@@ -370,6 +370,7 @@ export default class MasterRequest {
...this.normalizedCommandFlags.defaultFlags,
clientName: this.client.flags.clientName,
},
+ incorrectCaseFlags: new Set(),
shorthandFlags: new Set(),
}, target);
}

Enforce kebab casing for CLI flags