@bomb.sh/router 0.7.0 → 0.7.1

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.
package/src/lib/print.ts DELETED
@@ -1,216 +0,0 @@
1
- /**
2
- * 😵‍💫 This file has been vibe coded 😵‍💫
3
- */
4
-
5
- import type { AnyParam } from "./param.js";
6
- import type {
7
- AnyRoute,
8
- Help,
9
- Issue,
10
- MethodNotAllowed,
11
- RoutePath,
12
- UnprocessableContent,
13
- Version,
14
- } from "./types.js";
15
-
16
- export function printHelp<
17
- const H extends Help<RoutePath>,
18
- >(intent: H): string {
19
- let route = intent.definition;
20
- let subject = title(intent);
21
- let heading = route.version ? `${subject} ${route.version}` : subject;
22
- let children = route.phases.flatMap((phase) => phase.routes);
23
- let args: Row[] = [];
24
- let options: Row[] = [];
25
-
26
- for (let param of params(route)) {
27
- let syntax = param.cli.syntax;
28
- if (!syntax) {
29
- continue;
30
- }
31
-
32
- let row: Row = [syntax.label, param.description];
33
- if (syntax.type === "argument") {
34
- args.push(row);
35
- } else {
36
- options.push(row);
37
- }
38
- }
39
-
40
- let usage = [subject, "[OPTIONS]", ...args.map(([label]) => label)].join(
41
- " ",
42
- );
43
-
44
- if (children.length > 0) {
45
- usage += route.methods.includes("execute") ? " [COMMAND]" : " <COMMAND>";
46
- }
47
-
48
- let lines = [heading];
49
- if (route.description) {
50
- lines.push(...wrap(route.description, width));
51
- }
52
-
53
- lines.push("", "Usage:", ` ${usage}`);
54
-
55
- if (args.length > 0) {
56
- lines.push("", "Arguments:", ...list(args));
57
- }
58
-
59
- if (children.length > 0) {
60
- lines.push(
61
- "",
62
- "Commands:",
63
- ...list(children.map((child) => [child.name, child.description])),
64
- );
65
- }
66
-
67
- options.push(["-h, --help", "Print help"]);
68
- if (route.methods.includes("version")) {
69
- options.push(["-v, --version", "Print version"]);
70
- }
71
-
72
- lines.push("", "Options:", ...list(options));
73
-
74
- return lines.join("\n");
75
- }
76
-
77
- export function printVersion<
78
- const V extends Version<RoutePath>,
79
- >(intent: V): string {
80
- let version = intent.definition.version;
81
- if (!version) {
82
- throw new TypeError(`route ${JSON.stringify(intent.route)} has no version`);
83
- }
84
-
85
- return `${title(intent)} ${version}`;
86
- }
87
-
88
- export function printErrors(
89
- result: MethodNotAllowed | UnprocessableContent,
90
- ): string {
91
- let subject = title(result);
92
-
93
- switch (result.code) {
94
- case "method-not-allowed":
95
- return [
96
- `${subject} does not support ${result.method.toUpperCase()}`,
97
- "",
98
- "Available methods:",
99
- ...result.allowed.map((method) => ` ${method.toUpperCase()}`),
100
- ].join("\n");
101
-
102
- case "unprocessable-content": {
103
- return result.issues.map(problem).join("\n");
104
- }
105
- }
106
- }
107
-
108
- type Row = readonly [label: string, description?: string];
109
-
110
- function params(route: AnyRoute): AnyParam[] {
111
- return route.phases.flatMap((phase) =>
112
- Object.values(phase.model.params) as AnyParam[]
113
- );
114
- }
115
-
116
- function title(intent: {
117
- readonly path: readonly string[];
118
- readonly definition: AnyRoute;
119
- }): string {
120
- return intent.path.length > 0
121
- ? intent.path.join(" ")
122
- : intent.definition.name;
123
- }
124
-
125
- function list(rows: readonly Row[]): string[] {
126
- let size = Math.max(...rows.map(([label]) => label.length));
127
- let available = Math.max(24, width - size - 4);
128
- let lines: string[] = [];
129
-
130
- for (let [label, description] of rows) {
131
- let prefix = ` ${label.padEnd(size)}`;
132
- if (!description) {
133
- lines.push(prefix.trimEnd());
134
- continue;
135
- }
136
-
137
- let [first, ...rest] = wrap(description, available);
138
- lines.push(`${prefix} ${first}`);
139
- lines.push(...rest.map((line) => `${" ".repeat(size + 4)}${line}`));
140
- }
141
-
142
- return lines;
143
- }
144
-
145
- function wrap(text: string, size: number): string[] {
146
- let words = text.trim().split(/\s+/);
147
- let lines: string[] = [];
148
- let line = "";
149
-
150
- for (let word of words) {
151
- if (line.length === 0) {
152
- line = word;
153
- } else if (line.length + word.length + 1 <= size) {
154
- line += ` ${word}`;
155
- } else {
156
- lines.push(line);
157
- line = word;
158
- }
159
- }
160
-
161
- if (line) {
162
- lines.push(line);
163
- }
164
-
165
- return lines;
166
- }
167
-
168
- function problem(issue: Issue): string {
169
- let location = address(issue.path);
170
- return location ? `${location}: ${issue.message}` : message(issue.message);
171
- }
172
-
173
- function message(value: string): string {
174
- if (value.startsWith("unexpected ")) {
175
- let encoded = value.slice("unexpected ".length);
176
-
177
- try {
178
- let token = JSON.parse(encoded);
179
- if (typeof token === "string") {
180
- return `unexpected: \`${token.replaceAll("`", "\\`")}\``;
181
- }
182
- } catch {
183
- // Keep non-JSON diagnostics intact.
184
- }
185
- }
186
-
187
- return value;
188
- }
189
-
190
- function address(path: Issue["path"]): string | undefined {
191
- if (!path || path.length === 0) {
192
- return;
193
- }
194
-
195
- let result = "";
196
-
197
- for (let segment of path) {
198
- let key = typeof segment === "object" && segment !== null
199
- ? segment.key
200
- : segment;
201
-
202
- if (typeof key === "number") {
203
- result += `[${key}]`;
204
- } else if (typeof key === "symbol") {
205
- result += `[${String(key)}]`;
206
- } else if (/^[A-Za-z_$][\w$]*$/.test(key)) {
207
- result += result ? `.${key}` : key;
208
- } else {
209
- result += `[${JSON.stringify(key)}]`;
210
- }
211
- }
212
-
213
- return result;
214
- }
215
-
216
- const width = 80;
package/src/lib/read.ts DELETED
@@ -1,133 +0,0 @@
1
- import type { Maybe } from "./maybe.js";
2
- import type { AnyParam } from "./param.js";
3
- import { brand, type IdentityElement } from "./pipeline.js";
4
- import type { Result } from "./result.js";
5
- import type { Flag, Setter, Word } from "./tokenize.js";
6
- import type { Claim, TokenInput } from "./tokenizer.js";
7
-
8
- export type Symbol = Flag | Setter | Word;
9
-
10
- export type ReadCLI = (
11
- tokens: TokenInput<Symbol>,
12
- ) => CLIRead;
13
-
14
- export interface CLIBinding {
15
- readonly read: ReadCLI;
16
- readonly syntax?: CLISyntax;
17
- }
18
-
19
- export type CLISyntax =
20
- | { readonly type: "argument"; readonly label: string }
21
- | { readonly type: "option"; readonly label: string };
22
-
23
- export interface CLIRead {
24
- result: Result<Maybe<string | boolean>>;
25
- claim: Claim<Symbol>;
26
- }
27
-
28
- export interface CLIOptions {
29
- switch?: true;
30
- }
31
-
32
- export function cli(
33
- names: readonly string[],
34
- options: CLIOptions = {},
35
- ): IdentityElement<AnyParam> {
36
- const read: ReadCLI = (tokens) => {
37
- if (options.switch) {
38
- let s = tokens.claimOne((t): t is Flag => {
39
- return t.type === "flag" && names.includes(t.text);
40
- });
41
- let [flag] = s.tokens;
42
- return flag
43
- ? {
44
- result: {
45
- ok: true,
46
- value: { exists: true, value: true },
47
- issues: [],
48
- },
49
- claim: s,
50
- }
51
- : nothing(tokens);
52
- }
53
- let setter = tokens.claimOne((token): token is Setter => {
54
- return (token.type === "setter" && names.includes(`--${token.nameText}`));
55
- });
56
- let [token] = setter.tokens;
57
- if (token) {
58
- return {
59
- claim: setter,
60
- result: {
61
- ok: true,
62
- value: {
63
- exists: true,
64
- value: token.valueText,
65
- },
66
- issues: [],
67
- },
68
- };
69
- }
70
- let pair = tokens.claimPair((name, value) => {
71
- return name.type === "flag" && names.includes(name.text) &&
72
- value.type === "word";
73
- });
74
- let [, value] = pair.tokens;
75
- if (value) {
76
- return {
77
- claim: pair,
78
- result: {
79
- ok: true,
80
- value: {
81
- exists: true,
82
- value: value.text,
83
- },
84
- issues: [],
85
- },
86
- };
87
- }
88
- let bare = tokens.claimOne((t): t is Flag => {
89
- return t.type === "flag" && names.includes(t.text);
90
- });
91
- let [incomplete] = bare.tokens;
92
- if (incomplete) {
93
- return {
94
- claim: bare,
95
- result: {
96
- ok: false,
97
- issues: [{
98
- message: `${incomplete.text} requires a value`,
99
- }],
100
- },
101
- };
102
- }
103
- return nothing(tokens);
104
- };
105
-
106
- return brand<IdentityElement<AnyParam>>(
107
- (param: AnyParam) => ({
108
- ...param,
109
- cli: {
110
- read,
111
- syntax: {
112
- type: "option",
113
- label: options.switch
114
- ? names.join(", ")
115
- : `${names.join(", ")} <VALUE>`,
116
- },
117
- },
118
- }),
119
- );
120
- }
121
-
122
- function nothing(tokenizer: TokenInput<Symbol>): CLIRead {
123
- let claim = tokenizer.claimAll(() => false);
124
-
125
- return {
126
- claim,
127
- result: {
128
- ok: true,
129
- value: { exists: false },
130
- issues: [],
131
- },
132
- };
133
- }
package/src/lib/rest.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { Envs } from "./env.js";
2
- import type { Symbol } from "./read.js";
3
- import type { Tokenizer } from "./tokenizer.js";
4
- import type { Values } from "./values.js";
5
-
6
- export interface Rest {
7
- readonly tokens: Tokenizer<Symbol>;
8
- readonly values: Values;
9
- readonly envs: Envs;
10
- }
package/src/lib/result.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { Issue } from "./types.js";
2
-
3
- export type Result<T> = {
4
- ok: true;
5
- value: T;
6
- issues?: readonly Issue[];
7
- } | {
8
- ok: false;
9
- issues: readonly Issue[];
10
- };
package/src/lib/route.ts DELETED
@@ -1,77 +0,0 @@
1
- // deno-lint-ignore-file ban-types
2
- import {
3
- brand,
4
- type Check,
5
- type Fold,
6
- type Materialize,
7
- type MethodElement,
8
- type RoutesElement,
9
- type Unary,
10
- } from "./pipeline.js";
11
- import type { AnyRoute, Definition, Done, Route } from "./types.js";
12
-
13
- export type RouteZero<N extends string = string> = Route<
14
- N,
15
- "help",
16
- [Done<{}, []>]
17
- >;
18
-
19
- export function route<
20
- const N extends string,
21
- const E extends readonly Unary[],
22
- >(
23
- start: Definition<N>,
24
- ...elements: E & Check<RouteZero<N>, E>
25
- ): Materialize<Fold<RouteZero<N>, E>> {
26
- let zero: RouteZero<N> = {
27
- ...start,
28
- methods: ["help"],
29
- phases: [{
30
- model: {
31
- params: {},
32
- steps: [],
33
- },
34
- routes: [],
35
- values: [],
36
- envs: [],
37
- }],
38
- };
39
-
40
- return elements.reduce<unknown>(
41
- (value, element) => element(value as never),
42
- zero,
43
- ) as Materialize<Fold<RouteZero<N>, E>>;
44
- }
45
-
46
- export function version(semver: string): MethodElement<"version"> {
47
- return brand<MethodElement<"version">>((route: AnyRoute) => ({
48
- ...route,
49
- methods: [...route.methods, "version"] as const,
50
- version: semver,
51
- }));
52
- }
53
-
54
- export function executable(): MethodElement<"execute"> {
55
- return brand<MethodElement<"execute">>((route: AnyRoute) => ({
56
- ...route,
57
- methods: [...route.methods, "execute"] as const,
58
- }));
59
- }
60
-
61
- export function routes<const C extends readonly AnyRoute[]>(
62
- ...children: C
63
- ): RoutesElement<C> {
64
- return brand<RoutesElement<C>>((route: AnyRoute) => {
65
- let phases = [...route.phases];
66
- let phase = phases.pop()!;
67
- phases.push({
68
- ...phase,
69
- routes: [...phase.routes, ...children],
70
- });
71
-
72
- return {
73
- ...route,
74
- phases,
75
- };
76
- });
77
- }
package/src/lib/toggle.ts DELETED
@@ -1,132 +0,0 @@
1
- import { boolean as decode } from "./decode.js";
2
- import { dasherize } from "./dasherize.js";
3
- import {
4
- type AnyParam,
5
- type Param,
6
- param,
7
- type ParamModel,
8
- schema,
9
- } from "./param.js";
10
- import {
11
- brand,
12
- type Check,
13
- type Fold,
14
- type ModelElement,
15
- type Unary,
16
- } from "./pipeline.js";
17
- import type { CLIRead, ReadCLI } from "./read.js";
18
- import type { Flag } from "./tokenize.js";
19
- import type { AnyRoute, Definition, Schema } from "./types.js";
20
-
21
- export function toggle<
22
- const N extends string,
23
- const E extends readonly Unary[],
24
- >(
25
- named: Definition<N>,
26
- ...elements: E & Check<Zero<N>, E>
27
- ): ElementOf<N, Fold<Zero<N>, E>> {
28
- const added = elements.reduce<unknown>(
29
- (value, element) => element(value as never),
30
- {
31
- ...param(named, binding(named.name), schema(bool)),
32
- decode,
33
- },
34
- ) as AnyParam;
35
-
36
- return brand<ElementOf<N, Fold<Zero<N>, E>>>(
37
- (route: AnyRoute) => {
38
- let phases = [...route.phases];
39
- let phase = phases.pop()!;
40
- phases.push({
41
- ...phase,
42
- model: {
43
- params: {
44
- ...phase.model.params,
45
- [added.name]: added,
46
- },
47
- steps: phase.model.steps.concat((current, bindings) => ({
48
- ok: true,
49
- value: {
50
- ...current,
51
- [added.name]: bindings[added.name],
52
- },
53
- })),
54
- },
55
- });
56
-
57
- return {
58
- ...route,
59
- phases,
60
- };
61
- },
62
- );
63
- }
64
-
65
- type Zero<N extends string> = Param<N, boolean, "one">;
66
-
67
- type ElementOf<N extends string, P> = P extends
68
- Param<N, infer Model, infer _Cardinality> ? ModelElement<ParamModel<N, Model>>
69
- : never;
70
-
71
- function binding(name: string): <P extends AnyParam>(param: P) => P {
72
- const stem = dasherize(name);
73
- const yes = `--${stem}`;
74
- const no = `--no-${stem}`;
75
-
76
- return (param) => ({
77
- ...param,
78
- cli: {
79
- read: reader(name),
80
- syntax: {
81
- type: "option",
82
- label: `${yes}, ${no}`,
83
- },
84
- },
85
- });
86
- }
87
-
88
- function reader(name: string): ReadCLI {
89
- const stem = dasherize(name);
90
- const yes = `--${stem}`;
91
- const no = `--no-${stem}`;
92
-
93
- return (tokens): CLIRead => {
94
- let claim = tokens.claimOne((token): token is Flag => {
95
- return token.type === "flag" &&
96
- (token.text === yes || token.text === no);
97
- });
98
- let [flag] = claim.tokens;
99
-
100
- return flag
101
- ? {
102
- claim,
103
- result: {
104
- ok: true,
105
- value: { exists: true, value: flag.text === yes },
106
- issues: [],
107
- },
108
- }
109
- : {
110
- claim,
111
- result: {
112
- ok: true,
113
- value: { exists: false },
114
- issues: [],
115
- },
116
- };
117
- };
118
- }
119
-
120
- const bool: Schema<boolean> = {
121
- "~standard": {
122
- version: 1,
123
- vendor: "@bomb.sh/router",
124
- validate(value) {
125
- return typeof value === "undefined"
126
- ? { value: false }
127
- : typeof value === "boolean"
128
- ? { value }
129
- : { issues: [{ message: "expected boolean" }] };
130
- },
131
- },
132
- };
@@ -1,74 +0,0 @@
1
- export interface Token<T extends string> {
2
- type: T;
3
- index: number;
4
- text: string;
5
- }
6
- export type AnyToken =
7
- | Word
8
- | Flag
9
- | Setter
10
- | Separator
11
- | Literal;
12
-
13
- export interface Setter extends Token<"setter"> {
14
- nameText: string;
15
- valueText: string;
16
- }
17
-
18
- export interface Flag extends Token<"flag"> {
19
- flagText: string;
20
- flagType: "short" | "long";
21
- }
22
-
23
- export type Word = Token<"word">;
24
-
25
- export interface Separator extends Token<"separator"> {
26
- readonly text: "--";
27
- }
28
-
29
- export type Literal = Token<"literal">;
30
-
31
- export function tokenize(argv: string[]): readonly AnyToken[] {
32
- let quote = false;
33
- let tokens: AnyToken[] = [];
34
-
35
- for (let i = 0; i < argv.length; i++) {
36
- let index = i;
37
- let text = argv[i];
38
- if (quote) {
39
- tokens.push({ type: "literal", index, text });
40
- continue;
41
- }
42
- if (text === "--") {
43
- tokens.push({ type: "separator", index, text });
44
- quote = true;
45
- continue;
46
- }
47
- let matchSetter = setterMatch.exec(text);
48
- if (matchSetter?.groups) {
49
- let { nameText, valueText } = matchSetter.groups;
50
- tokens.push({ type: "setter", index, text, nameText, valueText });
51
- continue;
52
- }
53
- let matchFlag = flagMatch.exec(text);
54
- if (matchFlag?.groups) {
55
- let { prefix, flagText } = matchFlag.groups;
56
- tokens.push({
57
- type: "flag",
58
- index,
59
- text,
60
- flagText,
61
- flagType: prefix === "-" ? "short" : "long",
62
- });
63
- continue;
64
- }
65
- tokens.push({ type: "word", index, text });
66
- }
67
- return tokens;
68
- }
69
-
70
- // deno-lint-ignore no-invalid-regexp
71
- const flagMatch = /^(?<prefix>--?)(?<flagText>[^-=\s][^=\s]*)$/;
72
-
73
- // deno-lint-ignore no-invalid-regexp
74
- const setterMatch = /^--(?<nameText>[^-=\s][^=\s]*)=(?<valueText>[\s\S]*)$/;