openapi-typesafe 1.0.0-rc.5 → 1.0.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/adapters/redocly-loader/query-document-names.cjs +50 -0
  3. package/dist/adapters/redocly-loader/query-document-names.cjs.map +1 -0
  4. package/dist/adapters/redocly-loader/query-document-names.mjs +48 -0
  5. package/dist/adapters/redocly-loader/query-document-names.mjs.map +1 -0
  6. package/dist/adapters/redocly-loader/query-named-components.cjs +29 -37
  7. package/dist/adapters/redocly-loader/query-named-components.cjs.map +1 -1
  8. package/dist/adapters/redocly-loader/query-named-components.mjs +29 -37
  9. package/dist/adapters/redocly-loader/query-named-components.mjs.map +1 -1
  10. package/dist/adapters/redocly-loader/redocly-config-source.cjs +3 -0
  11. package/dist/adapters/redocly-loader/redocly-config-source.cjs.map +1 -1
  12. package/dist/adapters/redocly-loader/redocly-config-source.mjs +4 -1
  13. package/dist/adapters/redocly-loader/redocly-config-source.mjs.map +1 -1
  14. package/dist/adapters/redocly-loader/redocly-schema-loader.cjs +16 -7
  15. package/dist/adapters/redocly-loader/redocly-schema-loader.cjs.map +1 -1
  16. package/dist/adapters/redocly-loader/redocly-schema-loader.mjs +17 -8
  17. package/dist/adapters/redocly-loader/redocly-schema-loader.mjs.map +1 -1
  18. package/dist/adapters/redocly-loader/ref-access.cjs +9 -1
  19. package/dist/adapters/redocly-loader/ref-access.cjs.map +1 -1
  20. package/dist/adapters/redocly-loader/ref-access.mjs +10 -2
  21. package/dist/adapters/redocly-loader/ref-access.mjs.map +1 -1
  22. package/dist/adapters/redocly-loader/remote-fetch.cjs +16 -1
  23. package/dist/adapters/redocly-loader/remote-fetch.cjs.map +1 -1
  24. package/dist/adapters/redocly-loader/remote-fetch.mjs +16 -2
  25. package/dist/adapters/redocly-loader/remote-fetch.mjs.map +1 -1
  26. package/dist/api/cwd-option.cjs +58 -0
  27. package/dist/api/cwd-option.cjs.map +1 -0
  28. package/dist/api/cwd-option.mjs +52 -0
  29. package/dist/api/cwd-option.mjs.map +1 -0
  30. package/dist/api/default-ports.cjs +15 -7
  31. package/dist/api/default-ports.cjs.map +1 -1
  32. package/dist/api/default-ports.mjs +14 -8
  33. package/dist/api/default-ports.mjs.map +1 -1
  34. package/dist/api/document-run.cjs +2 -16
  35. package/dist/api/document-run.cjs.map +1 -1
  36. package/dist/api/document-run.mjs +2 -12
  37. package/dist/api/document-run.mjs.map +1 -1
  38. package/dist/api/legacy-utils.cjs +1 -1
  39. package/dist/api/legacy-utils.cjs.map +1 -1
  40. package/dist/api/legacy-utils.mjs +1 -1
  41. package/dist/api/legacy-utils.mjs.map +1 -1
  42. package/dist/api/openapi-ts.cjs.map +1 -1
  43. package/dist/api/openapi-ts.mjs.map +1 -1
  44. package/dist/api/option-errors.cjs +24 -0
  45. package/dist/api/option-errors.cjs.map +1 -0
  46. package/dist/api/option-errors.mjs +21 -0
  47. package/dist/api/option-errors.mjs.map +1 -0
  48. package/dist/api/ref-limit-options.cjs +19 -24
  49. package/dist/api/ref-limit-options.cjs.map +1 -1
  50. package/dist/api/ref-limit-options.mjs +15 -20
  51. package/dist/api/ref-limit-options.mjs.map +1 -1
  52. package/dist/application/transform/document/path-template-refs.cjs +12 -2
  53. package/dist/application/transform/document/path-template-refs.cjs.map +1 -1
  54. package/dist/application/transform/document/path-template-refs.mjs +12 -2
  55. package/dist/application/transform/document/path-template-refs.mjs.map +1 -1
  56. package/dist/application/transform/ref-pointer/generated-shape.cjs +28 -10
  57. package/dist/application/transform/ref-pointer/generated-shape.cjs.map +1 -1
  58. package/dist/application/transform/ref-pointer/generated-shape.mjs +29 -11
  59. package/dist/application/transform/ref-pointer/generated-shape.mjs.map +1 -1
  60. package/dist/application/transform/ref-pointer/original-index.cjs +1 -1
  61. package/dist/application/transform/ref-pointer/original-index.cjs.map +1 -1
  62. package/dist/application/transform/ref-pointer/original-index.mjs +1 -1
  63. package/dist/application/transform/ref-pointer/original-index.mjs.map +1 -1
  64. package/dist/application/transform/ref-pointer/property-shape.cjs +5 -0
  65. package/dist/application/transform/ref-pointer/property-shape.cjs.map +1 -1
  66. package/dist/application/transform/ref-pointer/property-shape.mjs +5 -1
  67. package/dist/application/transform/ref-pointer/property-shape.mjs.map +1 -1
  68. package/dist/application/transform/ref-pointer/typed-walk.cjs +11 -1
  69. package/dist/application/transform/ref-pointer/typed-walk.cjs.map +1 -1
  70. package/dist/application/transform/ref-pointer/typed-walk.mjs +12 -2
  71. package/dist/application/transform/ref-pointer/typed-walk.mjs.map +1 -1
  72. package/dist/cli/api-settings.cjs +3 -2
  73. package/dist/cli/api-settings.cjs.map +1 -1
  74. package/dist/cli/api-settings.mjs +2 -1
  75. package/dist/cli/api-settings.mjs.map +1 -1
  76. package/dist/cli/api-writes.cjs +7 -1
  77. package/dist/cli/api-writes.cjs.map +1 -1
  78. package/dist/cli/api-writes.mjs +7 -1
  79. package/dist/cli/api-writes.mjs.map +1 -1
  80. package/dist/cli/cli-arguments.cjs +25 -107
  81. package/dist/cli/cli-arguments.cjs.map +1 -1
  82. package/dist/cli/cli-arguments.mjs +18 -100
  83. package/dist/cli/cli-arguments.mjs.map +1 -1
  84. package/dist/cli/cli-errors.cjs +1 -0
  85. package/dist/cli/cli-errors.cjs.map +1 -1
  86. package/dist/cli/cli-errors.mjs +1 -0
  87. package/dist/cli/cli-errors.mjs.map +1 -1
  88. package/dist/cli/cli-flag-names.cjs +77 -0
  89. package/dist/cli/cli-flag-names.cjs.map +1 -0
  90. package/dist/cli/cli-flag-names.mjs +64 -0
  91. package/dist/cli/cli-flag-names.mjs.map +1 -0
  92. package/dist/cli/cli-output.cjs +7 -2
  93. package/dist/cli/cli-output.cjs.map +1 -1
  94. package/dist/cli/cli-output.mjs +7 -2
  95. package/dist/cli/cli-output.mjs.map +1 -1
  96. package/dist/cli/main.cjs +1 -1
  97. package/dist/cli/main.cjs.map +1 -1
  98. package/dist/cli/main.mjs +1 -1
  99. package/dist/cli/main.mjs.map +1 -1
  100. package/dist/cli/redocly-apis.cjs +14 -4
  101. package/dist/cli/redocly-apis.cjs.map +1 -1
  102. package/dist/cli/redocly-apis.mjs +14 -4
  103. package/dist/cli/redocly-apis.mjs.map +1 -1
  104. package/dist/cli/written-flags.cjs +83 -0
  105. package/dist/cli/written-flags.cjs.map +1 -0
  106. package/dist/cli/written-flags.mjs +77 -0
  107. package/dist/cli/written-flags.mjs.map +1 -0
  108. package/dist/domain/errors.cjs +1 -0
  109. package/dist/domain/errors.cjs.map +1 -1
  110. package/dist/domain/errors.mjs +1 -0
  111. package/dist/domain/errors.mjs.map +1 -1
  112. package/dist/domain/location-text.cjs +7 -1
  113. package/dist/domain/location-text.cjs.map +1 -1
  114. package/dist/domain/location-text.mjs +7 -1
  115. package/dist/domain/location-text.mjs.map +1 -1
  116. package/dist/index.d.cts +3 -0
  117. package/dist/index.d.mts +3 -0
  118. package/dist/index.d.ts +3 -0
  119. package/package.json +1 -1
  120. package/src/adapters/redocly-loader/query-document-names.ts +79 -0
  121. package/src/adapters/redocly-loader/query-named-components.ts +48 -49
  122. package/src/adapters/redocly-loader/redocly-config-source.ts +11 -1
  123. package/src/adapters/redocly-loader/redocly-schema-loader.ts +20 -6
  124. package/src/adapters/redocly-loader/ref-access.ts +16 -2
  125. package/src/adapters/redocly-loader/remote-fetch.ts +26 -0
  126. package/src/api/cwd-option.ts +76 -0
  127. package/src/api/default-ports.ts +33 -10
  128. package/src/api/document-run.ts +2 -17
  129. package/src/api/legacy-utils.ts +1 -1
  130. package/src/api/openapi-ts.ts +14 -1
  131. package/src/api/option-errors.ts +22 -0
  132. package/src/api/ref-limit-options.ts +17 -24
  133. package/src/application/ports/redocly-config-source.ts +5 -0
  134. package/src/application/transform/document/path-template-refs.ts +28 -7
  135. package/src/application/transform/ref-pointer/generated-shape.ts +57 -14
  136. package/src/application/transform/ref-pointer/original-index.ts +6 -3
  137. package/src/application/transform/ref-pointer/property-shape.ts +9 -0
  138. package/src/application/transform/ref-pointer/typed-walk.ts +26 -4
  139. package/src/cli/api-writes.ts +11 -3
  140. package/src/cli/cli-arguments.ts +42 -153
  141. package/src/cli/cli-errors.ts +1 -0
  142. package/src/cli/cli-flag-names.ts +99 -0
  143. package/src/cli/cli-output.ts +20 -6
  144. package/src/cli/main.ts +2 -1
  145. package/src/cli/redocly-apis.ts +21 -5
  146. package/src/cli/written-flags.ts +131 -0
  147. package/src/domain/errors.ts +1 -0
  148. package/src/domain/location-text.ts +17 -2
@@ -2,51 +2,28 @@ import { kebabCase } from "scule";
2
2
  import parser from "yargs-parser";
3
3
  import type { OutputFixName } from "../domain/output-fixes.js";
4
4
  import { cliError } from "./cli-errors.js";
5
+ import {
6
+ ALIASES,
7
+ BOOLEAN_FLAGS,
8
+ GENERATOR_FLAGS,
9
+ type GeneratorFlag,
10
+ type GeneratorFlags,
11
+ KNOWN_FLAGS,
12
+ STRING_FLAGS,
13
+ } from "./cli-flag-names.js";
14
+ import { MULTIPLE_SCHEMAS_DOCS_URL } from "./docs-links.js";
5
15
  import { fixNamesOf } from "./fix-option.js";
16
+ import {
17
+ rejectInvalidBooleans,
18
+ rejectNegatedValueFlags,
19
+ rejectRemovedFlags,
20
+ unknownFlagNames,
21
+ writtenFlags,
22
+ } from "./written-flags.js";
6
23
 
7
- /** The CLI's arguments, parsed as the original CLI parsed them (yargs-parser). */
8
-
9
- /** Boolean flags that become generator options (camelCase; yargs also accepts kebab-case). */
10
- export const GENERATOR_FLAGS = [
11
- "additionalProperties",
12
- "alphabetize",
13
- "arrayLength",
14
- "binaryAsBlob",
15
- "conditionalEnums",
16
- "dedupeEnums",
17
- "defaultNonNullable",
18
- "emptyObjectsUnknown",
19
- "enum",
20
- "enumValues",
21
- "excludeDeprecated",
22
- "exportType",
23
- "generatePathParams",
24
- "immutable",
25
- "lint",
26
- "makePathsEnum",
27
- "pathParamsAsTypes",
28
- "propertiesRequiredByDefault",
29
- "readWriteMarkers",
30
- "rootTypes",
31
- "rootTypesKeepCasing",
32
- "rootTypesNoSchemaPrefix",
33
- ] as const;
34
-
35
- export type GeneratorFlag = (typeof GENERATOR_FLAGS)[number];
36
-
37
- export type GeneratorFlags = Readonly<Partial<Record<GeneratorFlag, boolean>>>;
38
-
39
- // `contentNever` was a boolean flag of the original CLI that no code reads; it stays in the list
40
- // so that `--content-never <input>` keeps parsing as a flag and an input.
41
- const CAMEL_CASE_BOOLEAN_FLAGS = [...GENERATOR_FLAGS, "check", "contentNever", "help"];
24
+ export { GENERATOR_FLAGS, type GeneratorFlag, type GeneratorFlags } from "./cli-flag-names.js";
42
25
 
43
- // yargs-parser matches the `boolean` list by the exact spelling of the flag: with camelCase names
44
- // only, `--root-types schema.yaml` took the path as the flag's value (upstream #2523). Both
45
- // spellings are listed.
46
- const BOOLEAN_FLAGS = [
47
- ...CAMEL_CASE_BOOLEAN_FLAGS,
48
- ...CAMEL_CASE_BOOLEAN_FLAGS.map((flag) => kebabCase(flag)),
49
- ];
26
+ /** The CLI's arguments, parsed as the original CLI parsed them (yargs-parser). */
50
27
 
51
28
  /** The `$ref` limits as the user wrote them; unset flags are absent. */
52
29
  export interface CliRefLimits {
@@ -70,101 +47,9 @@ export interface CliArguments {
70
47
  readonly unknownFlags: readonly string[];
71
48
  }
72
49
 
73
- /** Flags of older versions that were removed or renamed, with the message for each. */
74
- const REMOVED_FLAGS: readonly (readonly [string, string])[] = [
75
- ["-ap", `The -ap alias has been deprecated. Use "--additional-properties" instead.`],
76
- ["--immutable-types", `The --immutable-types flag has been renamed to "--immutable".`],
77
- [
78
- "--support-array-length",
79
- `The --support-array-length flag has been renamed to "--array-length".`,
80
- ],
81
- ["-it", `The -it alias has been deprecated. Use "--immutable" instead.`],
82
- ["--redoc", `The --redoc config flag has been renamed to "--redocly" (or -c as shorthand).`],
83
- ];
84
-
85
50
  /** Flags that only work together with `--root-types`. */
86
51
  const ROOT_TYPES_FLAGS = ["rootTypesNoSchemaPrefix", "rootTypesKeepCasing"] as const;
87
52
 
88
- const OPTIONS_END = "--";
89
- const LONG_PREFIX = "--";
90
- const NEGATION_PREFIX = "--no-";
91
- const VALUE_SEPARATOR = "=";
92
- const BOOLEAN_VALUES: ReadonlySet<string> = new Set(["true", "false"]);
93
-
94
- /** A flag as written before `--`: its name (`--redoc`, `-o`) and the text after `=`, if any. */
95
- interface WrittenFlag {
96
- readonly name: string;
97
- readonly value: string | undefined;
98
- }
99
-
100
- function writtenFlags(argv: readonly string[]): WrittenFlag[] {
101
- const end = argv.indexOf(OPTIONS_END);
102
- return (end === -1 ? argv : argv.slice(0, end))
103
- .filter((arg) => arg.startsWith("-") && arg !== "-")
104
- .map((arg) => {
105
- const at = arg.indexOf(VALUE_SEPARATOR);
106
- return at === -1
107
- ? { name: arg, value: undefined }
108
- : { name: arg.slice(0, at), value: arg.slice(at + 1) };
109
- });
110
- }
111
-
112
- /**
113
- * A long flag as yargs-parser reads it: in kebab-case and without `--no-`, so `--immutableTypes`
114
- * and `--no-immutable-types` are `--immutable-types`. A short flag stays as written.
115
- */
116
- function canonicalFlagName(name: string): string {
117
- if (!name.startsWith(LONG_PREFIX)) {
118
- return name;
119
- }
120
- const bare = name.startsWith(NEGATION_PREFIX)
121
- ? name.slice(NEGATION_PREFIX.length)
122
- : name.slice(LONG_PREFIX.length);
123
- return `${LONG_PREFIX}${kebabCase(bare)}`;
124
- }
125
-
126
- /** An old flag fails by its name in any spelling, also with a value (`--redoc=redocly.yaml`). */
127
- function rejectRemovedFlags(flags: readonly WrittenFlag[]): void {
128
- const names = new Set(flags.map(({ name }) => canonicalFlagName(name)));
129
- for (const [flag, message] of REMOVED_FLAGS) {
130
- if (names.has(flag)) {
131
- throw cliError("CLI_FLAG_REMOVED", message);
132
- }
133
- }
134
- }
135
-
136
- const ALIASES = { redocly: ["c"], exportType: ["t"], output: ["o"] };
137
-
138
- /** The short aliases of boolean flags (`-t`), which take a value the same way (`-t=true`). */
139
- const BOOLEAN_ALIASES = Object.entries(ALIASES)
140
- .filter(([flag]) => CAMEL_CASE_BOOLEAN_FLAGS.includes(flag))
141
- .flatMap(([, aliases]) => aliases.map((alias) => `-${alias}`));
142
-
143
- const BOOLEAN_FLAG_NAMES: ReadonlySet<string> = new Set([
144
- ...BOOLEAN_FLAGS.map((flag) => `${LONG_PREFIX}${flag}`),
145
- ...BOOLEAN_ALIASES,
146
- ]);
147
-
148
- /**
149
- * A boolean flag takes `=true` or `=false`, and its `--no-` form takes no value. yargs-parser read
150
- * any other value as false (`--enum=1`, `--alphabetize=yes`), which turned the option off.
151
- */
152
- function rejectInvalidBooleans(flags: readonly WrittenFlag[]): void {
153
- for (const { name, value } of flags) {
154
- const negated = name.startsWith(NEGATION_PREFIX)
155
- ? `${LONG_PREFIX}${name.slice(NEGATION_PREFIX.length)}`
156
- : undefined;
157
- const isNegatedWithValue = negated !== undefined && BOOLEAN_FLAG_NAMES.has(negated);
158
- const isInvalid = BOOLEAN_FLAG_NAMES.has(name) && !BOOLEAN_VALUES.has(value ?? "true");
159
- if (value !== undefined && (isNegatedWithValue || isInvalid)) {
160
- throw cliError(
161
- "CLI_FLAG_INVALID",
162
- `${name}=${value}: a boolean flag takes no value, or =true or =false.`,
163
- );
164
- }
165
- }
166
- }
167
-
168
53
  /** Notices for flags that have no effect in this combination, printed before the run. */
169
54
  export function ineffectiveFlagNotices(flags: GeneratorFlags): string[] {
170
55
  if (flags.rootTypes === true) {
@@ -221,22 +106,6 @@ function refLimitsOf(parsed: parser.Arguments): CliRefLimits {
221
106
  };
222
107
  }
223
108
 
224
- /** String flags, in both spellings, so that a value such as `8080` is not read as a number. */
225
- const STRING_FLAGS = [
226
- "output",
227
- "redocly",
228
- "fix",
229
- ...["localRefRoot", "remoteRefHosts"].flatMap((flag) => [flag, kebabCase(flag)]),
230
- ];
231
-
232
- /** Every name yargs-parser can return for a flag this CLI reads. */
233
- const KNOWN_FLAGS = new Set<string>([
234
- ...BOOLEAN_FLAGS,
235
- ...STRING_FLAGS,
236
- ...Object.values(ALIASES).flat(),
237
- "version",
238
- ]);
239
-
240
109
  /**
241
110
  * Flags that no code reads. The original CLI ignored them silently, so a typo such as
242
111
  * `--alphabetise` gave other output with no hint; they still do not fail the run.
@@ -258,22 +127,42 @@ function writtenFlagName(key: string, argv: readonly string[]): string {
258
127
  return isNegated ? negated : `--${kebabCase(key)}`;
259
128
  }
260
129
 
130
+ /**
131
+ * The one schema argument. More than one fails with `CLI_INPUT_EXTRA`: the original CLI read the
132
+ * first and dropped the rest, so a glob the shell expanded (`g/*.yaml`) generated one schema.
133
+ */
134
+ function inputOf(positionals: readonly (string | number)[]): string | undefined {
135
+ if (positionals.length > 1) {
136
+ throw cliError(
137
+ "CLI_INPUT_EXTRA",
138
+ `Only one schema argument is allowed, but ${positionals.length} were given: ` +
139
+ `${positionals.join(" ")}. A shell expands an unquoted *; to generate several ` +
140
+ `schemas, list them under \`apis\` in redocly.yaml. See ${MULTIPLE_SCHEMAS_DOCS_URL}`,
141
+ );
142
+ }
143
+ return optionalString(positionals[0]);
144
+ }
145
+
261
146
  /**
262
147
  * Parse the arguments after `node cli.js`. Fails with `CLI_FLAG_REMOVED` for an old flag, with
263
- * `CLI_FLAG_INVALID` for a boolean flag with another value than true or false, and with
148
+ * `CLI_FLAG_INVALID` for a boolean flag with another value than true or false or a `--no-` form
149
+ * of a flag that takes a value, with `CLI_INPUT_EXTRA` for a second schema argument, and with
264
150
  * `OPTION_FIX_UNKNOWN` for an unknown `--fix` name.
265
151
  */
266
152
  export function parseCliArguments(argv: readonly string[]): CliArguments {
267
153
  const flags = writtenFlags(argv);
268
154
  rejectRemovedFlags(flags);
269
155
  rejectInvalidBooleans(flags);
156
+ rejectNegatedValueFlags(flags);
270
157
  const parsed = parser([...argv], {
271
- boolean: BOOLEAN_FLAGS,
158
+ boolean: [...BOOLEAN_FLAGS, ...unknownFlagNames(flags)],
272
159
  string: STRING_FLAGS,
273
160
  alias: ALIASES,
161
+ // A schema path is a file name: `1.10` is not the number 1.1.
162
+ configuration: { "parse-positional-numbers": false },
274
163
  });
275
164
  return {
276
- input: optionalString(parsed._[0]),
165
+ input: inputOf(parsed._),
277
166
  output: lastString(parsed.output),
278
167
  redocly: lastString(parsed.redocly),
279
168
  isCheck: parsed.check === true,
@@ -109,6 +109,7 @@ const CLI_ERROR_REPORTS: Readonly<Record<ErrorCode, CliErrorReport>> = {
109
109
  CLI_CONFIG_OUTPUT_MISSING: { exitCode: EXIT_CODES.USAGE },
110
110
  CLI_CHECK_OUTPUT_MISSING: { exitCode: EXIT_CODES.USAGE },
111
111
  CLI_INPUT_MISSING: { exitCode: EXIT_CODES.USAGE },
112
+ CLI_INPUT_EXTRA: { exitCode: EXIT_CODES.USAGE },
112
113
  CLI_OUTPUT_IS_INPUT: { exitCode: EXIT_CODES.USAGE },
113
114
  CLI_OUTPUT_DUPLICATE: { exitCode: EXIT_CODES.USAGE },
114
115
  CLI_OUTPUT_INVALID: { exitCode: EXIT_CODES.USAGE },
@@ -0,0 +1,99 @@
1
+ import { kebabCase } from "scule";
2
+
3
+ /** The names of the CLI's flags, in the spellings yargs-parser is told about. */
4
+
5
+ /** Boolean flags that become generator options (camelCase; yargs also accepts kebab-case). */
6
+ export const GENERATOR_FLAGS = [
7
+ "additionalProperties",
8
+ "alphabetize",
9
+ "arrayLength",
10
+ "binaryAsBlob",
11
+ "conditionalEnums",
12
+ "dedupeEnums",
13
+ "defaultNonNullable",
14
+ "emptyObjectsUnknown",
15
+ "enum",
16
+ "enumValues",
17
+ "excludeDeprecated",
18
+ "exportType",
19
+ "generatePathParams",
20
+ "immutable",
21
+ "lint",
22
+ "makePathsEnum",
23
+ "pathParamsAsTypes",
24
+ "propertiesRequiredByDefault",
25
+ "readWriteMarkers",
26
+ "rootTypes",
27
+ "rootTypesKeepCasing",
28
+ "rootTypesNoSchemaPrefix",
29
+ ] as const;
30
+
31
+ export type GeneratorFlag = (typeof GENERATOR_FLAGS)[number];
32
+
33
+ export type GeneratorFlags = Readonly<Partial<Record<GeneratorFlag, boolean>>>;
34
+
35
+ // `contentNever` was a boolean flag of the original CLI that no code reads; it stays in the list
36
+ // so that `--content-never <input>` keeps parsing as a flag and an input.
37
+ const CAMEL_CASE_BOOLEAN_FLAGS = [...GENERATOR_FLAGS, "check", "contentNever", "help"];
38
+
39
+ // yargs-parser matches the `boolean` list by the exact spelling of the flag: with camelCase names
40
+ // only, `--root-types schema.yaml` took the path as the flag's value (upstream #2523). Both
41
+ // spellings are listed.
42
+ export const BOOLEAN_FLAGS = [
43
+ ...CAMEL_CASE_BOOLEAN_FLAGS,
44
+ ...CAMEL_CASE_BOOLEAN_FLAGS.map((flag) => kebabCase(flag)),
45
+ ];
46
+
47
+ export const ALIASES = { redocly: ["c"], exportType: ["t"], output: ["o"] };
48
+
49
+ const CAMEL_CASE_STRING_FLAGS = ["output", "redocly", "fix", "localRefRoot", "remoteRefHosts"];
50
+
51
+ /** String flags, in both spellings, so that a value such as `8080` is not read as a number. */
52
+ export const STRING_FLAGS = [
53
+ ...CAMEL_CASE_STRING_FLAGS,
54
+ ...CAMEL_CASE_STRING_FLAGS.map((flag) => kebabCase(flag)),
55
+ ];
56
+
57
+ /** Every name yargs-parser can return for a flag this CLI reads. */
58
+ export const KNOWN_FLAGS: ReadonlySet<string> = new Set<string>([
59
+ ...BOOLEAN_FLAGS,
60
+ ...STRING_FLAGS,
61
+ ...Object.values(ALIASES).flat(),
62
+ "version",
63
+ ]);
64
+
65
+ export const LONG_PREFIX = "--";
66
+ export const SHORT_PREFIX = "-";
67
+ export const NEGATION_PREFIX = "--no-";
68
+
69
+ /** The short aliases of `flags`, as written on the command line (`-t`). */
70
+ function aliasesOf(flags: readonly string[]): string[] {
71
+ return Object.entries(ALIASES)
72
+ .filter(([flag]) => flags.includes(flag))
73
+ .flatMap(([, aliases]) => aliases.map((alias) => `${SHORT_PREFIX}${alias}`));
74
+ }
75
+
76
+ /** The boolean flags as written, `--` and `-` included; they take `=true` or `=false`. */
77
+ export const BOOLEAN_FLAG_NAMES: ReadonlySet<string> = new Set([
78
+ ...BOOLEAN_FLAGS.map((flag) => `${LONG_PREFIX}${flag}`),
79
+ ...aliasesOf(CAMEL_CASE_BOOLEAN_FLAGS),
80
+ ]);
81
+
82
+ /**
83
+ * The flags that take a value, as `canonicalFlagName` gives them: kebab-case, and the short
84
+ * aliases with `--`, since yargs-parser reads `--no-o` as the negation of `-o`.
85
+ */
86
+ export const VALUE_FLAG_NAMES: ReadonlySet<string> = new Set([
87
+ ...CAMEL_CASE_STRING_FLAGS.map((flag) => `${LONG_PREFIX}${kebabCase(flag)}`),
88
+ ...aliasesOf(CAMEL_CASE_STRING_FLAGS).map((alias) => `${SHORT_PREFIX}${alias}`),
89
+ ]);
90
+
91
+ /** The long flags this CLI reads, as `canonicalFlagName` gives them. */
92
+ export const KNOWN_LONG_FLAG_NAMES: ReadonlySet<string> = new Set(
93
+ [...KNOWN_FLAGS]
94
+ .filter((flag) => flag.length > 1)
95
+ .map((flag) => `${LONG_PREFIX}${kebabCase(flag)}`),
96
+ );
97
+
98
+ /** The short flags this CLI reads, without the `-`. */
99
+ export const KNOWN_SHORT_FLAGS: ReadonlySet<string> = new Set(Object.values(ALIASES).flat());
@@ -187,12 +187,15 @@ export function ensureOutputIsOwn(
187
187
  }
188
188
  }
189
189
 
190
- /** `CLI_OUTPUT_DUPLICATE` for `output`, which is also the output of the `others` (named). */
190
+ /**
191
+ * `CLI_OUTPUT_DUPLICATE` for `output`, which is also the output of the `others` (named). Only
192
+ * APIs share outputs, and the CLI names the API before the message (`ApiFailure`).
193
+ */
191
194
  export function duplicateOutputError(output: NamedOutput, others: readonly string[]): Error {
192
195
  return cliError(
193
196
  "CLI_OUTPUT_DUPLICATE",
194
- `The output ${output.output} of API ${output.source} is also the output of ` +
195
- `${others.join(", ")}: they would overwrite each other. Give each API its own output file.`,
197
+ `The output ${output.output} is also the output of ${others.join(", ")}: they would ` +
198
+ "overwrite each other. Give each API its own output file.",
196
199
  );
197
200
  }
198
201
 
@@ -216,13 +219,24 @@ function printDone(run: CliRun, output: OutputFile): void {
216
219
  run.deps.console.info(`🚀 ${target} ${duration}`);
217
220
  }
218
221
 
222
+ const CRLF = /\r\n/g;
223
+ const LF = "\n";
224
+
225
+ /**
226
+ * The text with LF line ends. Git with `core.autocrlf` checks a committed file out with CRLF on
227
+ * Windows, so `--check` compares lines, not line ends.
228
+ */
229
+ function withLfLineEnds(text: string): string {
230
+ return text.replace(CRLF, LF);
231
+ }
232
+
219
233
  /**
220
- * With `--check`, fail with `OUTPUT_STALE` when the file differs from `contents` and leave it
221
- * alone otherwise. Without it, write the file and print where it went.
234
+ * With `--check`, fail with `OUTPUT_STALE` when the file differs from `contents` (line ends
235
+ * aside) and leave it alone otherwise. Without it, write the file and print where it went.
222
236
  */
223
237
  export function emitOutput(run: CliRun, output: OutputFile, contents: string): void {
224
238
  if (run.args.isCheck) {
225
- if (run.deps.files.readText(output.file) !== contents) {
239
+ if (withLfLineEnds(run.deps.files.readText(output.file)) !== withLfLineEnds(contents)) {
226
240
  // The original message, then the file: with several APIs it tells which one is stale.
227
241
  throw cliError(
228
242
  "OUTPUT_STALE",
package/src/cli/main.ts CHANGED
@@ -27,9 +27,10 @@ function readVersion(): string {
27
27
  return packageJson.version;
28
28
  }
29
29
 
30
+ /** The style of the CLI's own lines, which go to stdout. */
30
31
  function terminalStyle(): TextStyle {
31
32
  const palette = colors.create();
32
- palette.enabled = terminalSupportsColor();
33
+ palette.enabled = terminalSupportsColor("stdout");
33
34
  return {
34
35
  bold: (text) => palette.bold(text),
35
36
  green: (text) => palette.green(text),
@@ -104,7 +104,8 @@ interface ApiRun {
104
104
  }
105
105
 
106
106
  /**
107
- * The generator options of one API: its own settings win over the CLI's flags. A flag that has
107
+ * The generator options of one API: its own settings win over the CLI's flags, and its own lint
108
+ * rules in `redocly.yaml` over the root's (upstream used the root's only). A flag that has
108
109
  * no effect with the merged flags is named for this API, since its settings may turn on the flag
109
110
  * that the command line lacks, or set one that has no effect themselves.
110
111
  */
@@ -119,7 +120,7 @@ function apiOptions(run: CliRun, apiRun: ApiRun, target: ApiTarget): GeneratorOp
119
120
  ...apiRun.refLimits,
120
121
  ...apiRefLimitOptions(name, settings, configRoot),
121
122
  ...fixOption(apiFixNames(name, settings) ?? run.args.fix),
122
- redocly: apiRun.redocly,
123
+ redocly: run.deps.redoclyConfigs.forApi(apiRun.redocly, name),
123
124
  };
124
125
  }
125
126
 
@@ -146,9 +147,12 @@ async function generateApi(run: CliRun, apiRun: ApiRun, target: ApiTarget): Prom
146
147
  run.deps.logger.warn(warning);
147
148
  }
148
149
  const output = apiOutputOf(target.name, target.settings);
149
- const outputFile = outputFileOf(run, { source: target.name, output }, target.configRoot);
150
- ensureOutputIsNotSchema(run, outputFile, files.schemas);
151
- ensureOutputIsOwn(run, outputFile, otherOutputs(files, target.name));
150
+ const outputFile = failingAsApi(target.name, () => {
151
+ const file = outputFileOf(run, { source: target.name, output }, target.configRoot);
152
+ ensureOutputIsNotSchema(run, file, files.schemas);
153
+ ensureOutputIsOwn(run, file, otherOutputs(files, target.name));
154
+ return file;
155
+ });
152
156
  const source = resolveLocationUrl(target.root, target.configRoot);
153
157
  const options = apiOptions(run, apiRun, target);
154
158
  try {
@@ -159,6 +163,18 @@ async function generateApi(run: CliRun, apiRun: ApiRun, target: ApiTarget): Prom
159
163
  }
160
164
  }
161
165
 
166
+ /**
167
+ * The result of a step of one API, whose failure is reported with the API's name. Errors of an
168
+ * API's settings name the API in their own message and are not wrapped.
169
+ */
170
+ function failingAsApi<T>(apiName: string, step: () => T): T {
171
+ try {
172
+ return step();
173
+ } catch (error) {
174
+ throw new ApiFailure(apiName, error);
175
+ }
176
+ }
177
+
162
178
  /**
163
179
  * The schema and the output of every API, and the config file itself. A location that is no
164
180
  * text or no valid URL is left out: it is no file that an output could be, and its own API fails
@@ -0,0 +1,131 @@
1
+ import { kebabCase } from "scule";
2
+ import { cliError } from "./cli-errors.js";
3
+ import {
4
+ BOOLEAN_FLAG_NAMES,
5
+ KNOWN_LONG_FLAG_NAMES,
6
+ KNOWN_SHORT_FLAGS,
7
+ LONG_PREFIX,
8
+ NEGATION_PREFIX,
9
+ SHORT_PREFIX,
10
+ VALUE_FLAG_NAMES,
11
+ } from "./cli-flag-names.js";
12
+
13
+ /**
14
+ * Checks of the flags as the user wrote them, before yargs-parser reads them: yargs-parser accepts
15
+ * any spelling and value, so what it cannot tell apart is checked here.
16
+ */
17
+
18
+ /** Flags of older versions that were removed or renamed, with the message for each. */
19
+ const REMOVED_FLAGS: readonly (readonly [string, string])[] = [
20
+ ["-ap", `The -ap alias has been deprecated. Use "--additional-properties" instead.`],
21
+ ["--immutable-types", `The --immutable-types flag has been renamed to "--immutable".`],
22
+ [
23
+ "--support-array-length",
24
+ `The --support-array-length flag has been renamed to "--array-length".`,
25
+ ],
26
+ ["-it", `The -it alias has been deprecated. Use "--immutable" instead.`],
27
+ ["--redoc", `The --redoc config flag has been renamed to "--redocly" (or -c as shorthand).`],
28
+ ];
29
+
30
+ const OPTIONS_END = "--";
31
+ const VALUE_SEPARATOR = "=";
32
+ const BOOLEAN_VALUES: ReadonlySet<string> = new Set(["true", "false"]);
33
+ const LETTER = /^[a-z]$/i;
34
+
35
+ /** A flag as written before `--`: its name (`--redoc`, `-o`) and the text after `=`, if any. */
36
+ export interface WrittenFlag {
37
+ readonly name: string;
38
+ readonly value: string | undefined;
39
+ }
40
+
41
+ export function writtenFlags(argv: readonly string[]): WrittenFlag[] {
42
+ const end = argv.indexOf(OPTIONS_END);
43
+ return (end === -1 ? argv : argv.slice(0, end))
44
+ .filter((arg) => arg.startsWith(SHORT_PREFIX) && arg !== SHORT_PREFIX)
45
+ .map((arg) => {
46
+ const at = arg.indexOf(VALUE_SEPARATOR);
47
+ return at === -1
48
+ ? { name: arg, value: undefined }
49
+ : { name: arg.slice(0, at), value: arg.slice(at + 1) };
50
+ });
51
+ }
52
+
53
+ /**
54
+ * A long flag as yargs-parser reads it: in kebab-case and without `--no-`, so `--immutableTypes`
55
+ * and `--no-immutable-types` are `--immutable-types`. A short flag stays as written.
56
+ */
57
+ function canonicalFlagName(name: string): string {
58
+ if (!name.startsWith(LONG_PREFIX)) {
59
+ return name;
60
+ }
61
+ const bare = name.startsWith(NEGATION_PREFIX)
62
+ ? name.slice(NEGATION_PREFIX.length)
63
+ : name.slice(LONG_PREFIX.length);
64
+ return `${LONG_PREFIX}${kebabCase(bare)}`;
65
+ }
66
+
67
+ /** An old flag fails by its name in any spelling, also with a value (`--redoc=redocly.yaml`). */
68
+ export function rejectRemovedFlags(flags: readonly WrittenFlag[]): void {
69
+ const names = new Set(flags.map(({ name }) => canonicalFlagName(name)));
70
+ for (const [flag, message] of REMOVED_FLAGS) {
71
+ if (names.has(flag)) {
72
+ throw cliError("CLI_FLAG_REMOVED", message);
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * A boolean flag takes `=true` or `=false`, and its `--no-` form takes no value. yargs-parser read
79
+ * any other value as false (`--enum=1`, `--alphabetize=yes`), which turned the option off.
80
+ */
81
+ export function rejectInvalidBooleans(flags: readonly WrittenFlag[]): void {
82
+ for (const { name, value } of flags) {
83
+ const negated = name.startsWith(NEGATION_PREFIX)
84
+ ? `${LONG_PREFIX}${name.slice(NEGATION_PREFIX.length)}`
85
+ : undefined;
86
+ const isNegatedWithValue = negated !== undefined && BOOLEAN_FLAG_NAMES.has(negated);
87
+ const isInvalid = BOOLEAN_FLAG_NAMES.has(name) && !BOOLEAN_VALUES.has(value ?? "true");
88
+ if (value !== undefined && (isNegatedWithValue || isInvalid)) {
89
+ throw cliError(
90
+ "CLI_FLAG_INVALID",
91
+ `${name}=${value}: a boolean flag takes no value, or =true or =false.`,
92
+ );
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * A flag that takes a value has no `--no-` form: yargs-parser gave it the value false, which the
99
+ * CLI read as the text "false" (`--no-output` wrote a file named `false`).
100
+ */
101
+ export function rejectNegatedValueFlags(flags: readonly WrittenFlag[]): void {
102
+ for (const { name } of flags) {
103
+ if (name.startsWith(NEGATION_PREFIX) && VALUE_FLAG_NAMES.has(canonicalFlagName(name))) {
104
+ throw cliError(
105
+ "CLI_FLAG_INVALID",
106
+ `${name}: a flag that takes a value has no --no- form. Leave the flag out instead.`,
107
+ );
108
+ }
109
+ }
110
+ }
111
+
112
+ /** The bare names of the unknown flags in a written long flag or a group of short flags. */
113
+ function unknownNamesOf({ name }: WrittenFlag): string[] {
114
+ if (name.startsWith(LONG_PREFIX)) {
115
+ return KNOWN_LONG_FLAG_NAMES.has(canonicalFlagName(name))
116
+ ? []
117
+ : [name.slice(LONG_PREFIX.length)];
118
+ }
119
+ return [...name.slice(SHORT_PREFIX.length)].filter(
120
+ (letter) => LETTER.test(letter) && !KNOWN_SHORT_FLAGS.has(letter),
121
+ );
122
+ }
123
+
124
+ /**
125
+ * The unknown flags, to declare as boolean: yargs-parser gives an undeclared flag the next
126
+ * argument as its value, so `--alphabetise api.yaml` took the schema path and read stdin. An
127
+ * unknown flag is ignored, so it takes a value only as `--flag=value`.
128
+ */
129
+ export function unknownFlagNames(flags: readonly WrittenFlag[]): string[] {
130
+ return [...new Set(flags.flatMap(unknownNamesOf))];
131
+ }
@@ -38,6 +38,7 @@ export const ERROR_CODES = {
38
38
  CLI_CONFIG_OUTPUT_MISSING: "CLI_CONFIG_OUTPUT_MISSING",
39
39
  CLI_CHECK_OUTPUT_MISSING: "CLI_CHECK_OUTPUT_MISSING",
40
40
  CLI_INPUT_MISSING: "CLI_INPUT_MISSING",
41
+ CLI_INPUT_EXTRA: "CLI_INPUT_EXTRA",
41
42
  CLI_OUTPUT_IS_INPUT: "CLI_OUTPUT_IS_INPUT",
42
43
  CLI_OUTPUT_DUPLICATE: "CLI_OUTPUT_DUPLICATE",
43
44
  CLI_OUTPUT_INVALID: "CLI_OUTPUT_INVALID",
@@ -6,7 +6,22 @@
6
6
  */
7
7
  const URL_SCHEME = /^[a-z][a-z\d+.-]+:/i;
8
8
 
9
- /** Whether the location is written as a URL, not as a file path. */
9
+ /** The schemes the generator reads, which mark a URL also without `//` (`file:api.yaml`). */
10
+ const READ_SCHEMES: ReadonlySet<string> = new Set(["file:", "http:", "https:"]);
11
+
12
+ const AUTHORITY_START = "//";
13
+
14
+ /**
15
+ * Whether the location is written as a URL, not as a file path: a scheme the generator reads, or
16
+ * any scheme followed by `//` (`s3://bucket/api.yaml`). Another text before a colon is part of a
17
+ * file name, as it is on POSIX file systems (`out:v2.ts`, `spec:v2.yaml`).
18
+ */
10
19
  export function hasUrlScheme(location: string): boolean {
11
- return URL_SCHEME.test(location);
20
+ const scheme = URL_SCHEME.exec(location)?.[0];
21
+ if (scheme === undefined) {
22
+ return false;
23
+ }
24
+ return (
25
+ READ_SCHEMES.has(scheme.toLowerCase()) || location.startsWith(AUTHORITY_START, scheme.length)
26
+ );
12
27
  }