roundel 0.0.1 → 0.2.0
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/LICENSE +21 -0
- package/README.md +168 -14
- package/dist/chalk.d.ts +82 -0
- package/dist/chalk.js +143 -0
- package/dist/contrast.d.ts +24 -0
- package/dist/contrast.js +35 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +5 -7
- package/dist/plugin.d.ts +76 -0
- package/dist/plugin.js +71 -0
- package/dist/policy.d.ts +93 -0
- package/dist/policy.js +28 -0
- package/dist/schema.json +1 -0
- package/dist/theme.d.ts +13 -0
- package/dist/theme.js +84 -0
- package/dist/tokens.d.ts +22 -0
- package/dist/tokens.js +35 -0
- package/package.json +36 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ofri Peretz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/ofri-peretz/burgee/tree/main/packages/roundel" target="blank">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/ofri-peretz/burgee/main/brand-assets/roundel-lockup.svg" />
|
|
5
|
+
<img src="https://raw.githubusercontent.com/ofri-peretz/burgee/main/brand-assets/roundel-lockup-light.svg" alt="roundel" width="360" />
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
2
9
|
|
|
3
|
-
|
|
4
|
-
|
|
10
|
+
<p align="center">
|
|
11
|
+
The colours a CLI carries — error and hint, not red and blue.
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
<a href="https://www.npmjs.com/package/roundel"><img src="https://img.shields.io/npm/v/roundel?style=flat-square&color=0a6b47" alt="npm version" /></a>
|
|
16
|
+
<a href="https://www.npmjs.com/package/roundel"><img src="https://img.shields.io/npm/dm/roundel?style=flat-square" alt="npm downloads" /></a>
|
|
17
|
+
<img src="https://img.shields.io/badge/runtime%20dependencies-0-0a6b47?style=flat-square" alt="Zero runtime dependencies" />
|
|
18
|
+
<img src="https://img.shields.io/badge/Node.js-24+-green.svg?style=flat-square" alt="Node.js 24+" />
|
|
19
|
+
<img src="https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square" alt="License: MIT" />
|
|
20
|
+
</p>
|
|
21
|
+
|
|
22
|
+
chalk gives you `red`; picocolors gives you `red` for fewer bytes. Neither gives you
|
|
23
|
+
`error`, and each decides on its own whether the terminal has colour — which is why a
|
|
24
|
+
program's spinner, prompt and help so often disagree. **roundel** is the colours a CLI
|
|
25
|
+
carries: one output policy decided once from the runtime, nine semantic tokens over
|
|
26
|
+
`util.styleText`, and a theme that changes them all together, contrast-checked before it
|
|
27
|
+
flies — plus chalk's API over the same tokens, for the program that is not ready to give
|
|
28
|
+
chalk up. Zero dependencies, five subpaths, each costing only itself.
|
|
5
29
|
|
|
6
30
|
A **roundel** is a flag's colours carried onto another surface — the rings on an aircraft's
|
|
7
31
|
wing, the London Underground sign. Identity, expressed purely in colour, on something that is
|
|
@@ -9,16 +33,146 @@ not a flag. That is what this package is for a command-line program: not `red` a
|
|
|
9
33
|
but `error`, `hint`, `command` and `flag`, the colours that mean *you*, carried onto the
|
|
10
34
|
terminal as a theme.
|
|
11
35
|
|
|
12
|
-
##
|
|
36
|
+
## Start here
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install roundel
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
import { fly } from 'roundel/theme';
|
|
44
|
+
import { command, error, hint } from 'roundel/tokens';
|
|
45
|
+
|
|
46
|
+
// Once, at startup. The runtime is yours to describe; nothing here reads `process`.
|
|
47
|
+
fly({}, { env: process.env, isTTY: { stdout: Boolean(process.stdout.isTTY) } });
|
|
48
|
+
|
|
49
|
+
console.error(`${error('missing --name')} ${hint('try')} ${command('greet --name ada')}`);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Through a pipe with nothing asked, under `NO_COLOR`, or with `--json`, every token returns
|
|
53
|
+
its input unchanged. On a terminal it styles, at the level the terminal has — and on a pipe
|
|
54
|
+
too when the user said so with `FORCE_COLOR` or `--color`.
|
|
55
|
+
|
|
56
|
+
## What is here
|
|
57
|
+
|
|
58
|
+
| Subpath | Gives you |
|
|
59
|
+
| :-- | :-- |
|
|
60
|
+
| `roundel/policy` | `outputMode(rt, { json })` → `tty \| pipe \| json \| accessible \| ci` and `colorLevel(rt)` → `0 \| 1 \| 2 \| 3`. Pure over `{ env, isTTY: { stdout }, argv? }`; the only place in the package that reads `NO_COLOR`, `FORCE_COLOR`, `TERM`, `COLORTERM`, `CI`, `CLI_ACCESSIBLE`, the CI vendor variables or a `--color` flag. |
|
|
61
|
+
| `roundel/tokens` | `error warn ok hint muted command flag value heading` — each `(s: string) => string`, the identity until `fly()` has decided a level above 0. |
|
|
62
|
+
| `roundel/plugin` | `register(plugin)`, `theme()`, `contributions()`. A plugin is the family's one plain object; roundel keeps its `tokens` and ignores every key it does not understand, so the same object works on any subset of the family that is installed. |
|
|
63
|
+
| `roundel/theme` | `fly(theme, rt)`. A theme maps tokens to `styleText` format names (`['bold', 'underline']`) or a `#rrggbb`, and declares the `ground` it will be read on. Hex is truecolor at level 3 and falls back to the nearest of 256 or 16 colours below it. |
|
|
64
|
+
| `roundel/contrast` | `contrast(a, b)`, `luminance(hex)`, `AA` — the WCAG 2.2 maths `fly()` checks with. |
|
|
65
|
+
| `roundel/chalk` | chalk 6's API — `chalk.red.bold(s)`, `chalk.hex('#…')`, `new Chalk({ level })`, `chalkStderr`, `supportsColor`, the name lists — over the tokens' emitter and the policy's level. Graded by chalk's own suite; see below. |
|
|
66
|
+
|
|
67
|
+
### The policy
|
|
68
|
+
|
|
69
|
+
First match wins: `json` if the run asked for it; `accessible` if `CLI_ACCESSIBLE`; `ci` if
|
|
70
|
+
`CI` and not a TTY; `pipe` if not a TTY; else `tty`. **The mode decides redraws — spinners,
|
|
71
|
+
progress, anything that rewrites a line — and never the colour level.**
|
|
72
|
+
|
|
73
|
+
The level is chalk's, and it obeys the user's explicit instruction in any mode: `NO_COLOR`
|
|
74
|
+
wins outright; then `FORCE_COLOR=0`, which supports-color settles before it reads any flag,
|
|
75
|
+
so **an explicit "colour off" is never overridden into colour on** — `FORCE_COLOR=0` with
|
|
76
|
+
`--color=256` is 0, not 2; then the `--color` flags (`--color=256`, `--color=16m`,
|
|
77
|
+
`--no-color`, `--no-colors`, `--color=never`… both spellings, as has-flag has them) when the
|
|
78
|
+
caller hands the policy its `argv`; then `FORCE_COLOR`, which names an exact level
|
|
79
|
+
(`FORCE_COLOR=2` is 2, not "2 or better") or, as `true` or empty, only turns colour on and
|
|
80
|
+
lets the environment decide it. `--json` is the one output the level never enters:
|
|
81
|
+
structured text carries no escapes.
|
|
82
|
+
|
|
83
|
+
With no instruction at all the order is supports-color's own, deliberately: **a pipe is
|
|
84
|
+
`0`** — a pipe nobody asked to colour is a file or another program's stdin — and
|
|
85
|
+
**accessible mode is `0` too**, because `CLI_ACCESSIBLE` is itself an instruction from a
|
|
86
|
+
human and ANSI colour is noise to a screen reader; an explicit ask still colours either of
|
|
87
|
+
them. Azure Pipelines (`TF_BUILD` *and* `AGENT_NAME`) is the single exception on a pipe,
|
|
88
|
+
exactly where chalk puts it.
|
|
89
|
+
Once colour *is* being detected, on a terminal or because the run asked, `TERM=dumb` is the
|
|
90
|
+
floor, a `CI` run gets its vendor's level (GitHub and Gitea Actions and CircleCI at
|
|
91
|
+
truecolor; Travis, AppVeyor, GitLab, Buildkite, Drone and Codeship at 16), and anything else
|
|
92
|
+
is what `TERM` and `COLORTERM` report. So `FORCE_COLOR=true` on GitHub Actions gives you
|
|
93
|
+
truecolor logs, and nobody else's pipe changes.
|
|
94
|
+
|
|
95
|
+
One policy, one answer: no two components in a program can reach different conclusions,
|
|
96
|
+
which is the whole point (clack #286).
|
|
97
|
+
|
|
98
|
+
### The theme
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
fly(
|
|
102
|
+
{
|
|
103
|
+
ground: '#0a0a0a', // what the hex tokens are checked against; near-black by default
|
|
104
|
+
error: '#f4794a', // truecolor, checked at 4.5:1 against the ground, or fly() throws
|
|
105
|
+
heading: ['bold', 'underline'], // the terminal's own palette — never checked or claimed
|
|
106
|
+
},
|
|
107
|
+
runtime,
|
|
108
|
+
);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The defaults carry the burgee brand — rock for `error`, juniper for `ok`, each in whichever
|
|
112
|
+
of its deep or lifted variants reads better on the declared ground — and format names for
|
|
113
|
+
the other seven. A hex token below 4.5:1 is refused at every level, not only on truecolor
|
|
114
|
+
terminals, so a theme that would not read fails in CI rather than on one laptop. The 16-
|
|
115
|
+
and 256-colour fallbacks are the user's terminal palette and are not checked: a number
|
|
116
|
+
there would be invented.
|
|
117
|
+
|
|
118
|
+
## Migrating from chalk
|
|
119
|
+
|
|
120
|
+
```diff
|
|
121
|
+
- import chalk from 'chalk';
|
|
122
|
+
+ import chalk from 'roundel/chalk';
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Identical.** The chain (`chalk.red.bold.underline(s)`, every modifier, the sixteen
|
|
126
|
+
colours and their `Bright` variants, backgrounds, chalk 6's underline styles and colours),
|
|
127
|
+
`rgb`/`hex`/`ansi256` and their `bg`/`underline` forms with chalk's own downsampling at
|
|
128
|
+
levels 2 and 1, nesting and line-break handling byte for byte, `chalk.level` (get and set,
|
|
129
|
+
validated), `new Chalk({ level })`, `chalkStderr`, `supportsColor` / `supportsColorStderr`,
|
|
130
|
+
`modifierNames` / `foregroundColorNames` / `backgroundColorNames` / `underlineColorNames` /
|
|
131
|
+
`colorNames`, `visible`, `reset`, and `Function.prototype` on every link. ESM with a
|
|
132
|
+
`default` condition, so `require('roundel/chalk')` works too.
|
|
133
|
+
|
|
134
|
+
**Different.**
|
|
135
|
+
|
|
136
|
+
- **The level is per façade.** `chalk.level = 0` silences `roundel/chalk` and nothing else;
|
|
137
|
+
the tokens and the theme keep reading the policy. chalk's global mutable level is why chalk
|
|
138
|
+
and ora disagree about the same terminal, and it stops at this door.
|
|
139
|
+
- **The policy decides colour.** The level is detected once at import through
|
|
140
|
+
`colorLevel()`, reading the same `NO_COLOR`, `FORCE_COLOR`, `--color`, CI-vendor, `TERM`
|
|
141
|
+
and `COLORTERM` rules chalk does — see [The policy](#the-policy). Every other package in
|
|
142
|
+
the family reads the same answer, so `roundel/chalk` and a spinner cannot disagree about
|
|
143
|
+
the terminal the way chalk and ora do.
|
|
144
|
+
- **No template literal.** `chalk\`{red x}\`` was removed in chalk 5 and is not resurrected.
|
|
145
|
+
- **No emulator allow-list.** chalk's per-terminal-program detection (`TERM_PROGRAM`,
|
|
146
|
+
kitty, ghostty, wezterm, TeamCity, the Windows build number) is not reproduced; a program
|
|
147
|
+
on one of those terminals that wants colour asks for it with `FORCE_COLOR` or `--color`.
|
|
148
|
+
|
|
149
|
+
**Graded by chalk's own suite**, vendored at 6.0.0 into `compat-oracle` and run unedited
|
|
150
|
+
through a generated shim: **58 of 58 tests (100.0%) on 2026-09-08**, alongside the same
|
|
151
|
+
suite scoring 58 / 58 against real chalk in the same run.
|
|
152
|
+
|
|
153
|
+
## Weight
|
|
154
|
+
|
|
155
|
+
Every subpath is a lock, not a convention. `roundel/tokens` reaches 3,258 bytes on disk
|
|
156
|
+
(its ceiling is picocolors, 3.3 KB); `roundel/policy` 1,972; `roundel/theme` 6,271;
|
|
157
|
+
`roundel/plugin` 2,812 and reaching no module at all;
|
|
158
|
+
`roundel/contrast` 1,250; `roundel/chalk` 9,311 (its ceiling is chalk 6.0.0's own 9,370,
|
|
159
|
+
before the ansi-styles and supports-color chalk also ships). Importing one never loads
|
|
160
|
+
another — the tokens never carry the theme, the theme never carries the tokens, chalk
|
|
161
|
+
carries neither — and `sideEffects: false` lets a bundler drop what a program does not use.
|
|
162
|
+
ESM with a `default` condition, so `require('roundel/tokens')` works from CommonJS on
|
|
163
|
+
Node ≥ 24.
|
|
164
|
+
|
|
165
|
+
## What is next
|
|
166
|
+
|
|
167
|
+
- **`roundel/import`** — `fromBase16(scheme)` and `fromITerm(plist)`: a theme from the two
|
|
168
|
+
largest corpora of terminal palettes, contrast-checked on the way in.
|
|
169
|
+
|
|
170
|
+
---
|
|
13
171
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
migration is one import and your tests are unchanged.
|
|
20
|
-
- **Zero dependencies**, and every subpath measured against the lightest incumbent it replaces.
|
|
172
|
+
Part of the [burgee](https://github.com/ofri-peretz/burgee) family: a CLI on
|
|
173
|
+
[burgee](https://www.npmjs.com/package/burgee) declares what it is, roundel carries its
|
|
174
|
+
colours, [flagstaff](https://www.npmjs.com/package/flagstaff) flies it, and
|
|
175
|
+
[caique](https://www.npmjs.com/package/caique) answers back. Each is an independent package;
|
|
176
|
+
none requires the others.
|
|
21
177
|
|
|
22
|
-
|
|
23
|
-
what it is, roundel carries its colours, flagstaff flies it, answerback replies. Each is
|
|
24
|
-
an independent package; none requires the others.
|
|
178
|
+
MIT © Ofri Peretz — see [LICENSE](./LICENSE).
|
package/dist/chalk.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `roundel/chalk` (R6): chalk 6's public API — the chainable builder, the mutable `level`,
|
|
3
|
+
* `Chalk`, `chalkStderr`, `supportsColor` — over the tokens' emitter and the policy's
|
|
4
|
+
* level, graded by chalk's own suite vendored into `compat-oracle`.
|
|
5
|
+
*
|
|
6
|
+
* chalk's mutable `level` is honoured here and nowhere else: `chalk.level = 0` silences
|
|
7
|
+
* this façade, not the tokens, which read the policy. The template literal
|
|
8
|
+
* (`chalk\`{red x}\``) went with chalk 5 and is not resurrected. Not one escape is
|
|
9
|
+
* written here: parameters are computed, `sgr()` in tokens emits them (R3).
|
|
10
|
+
*
|
|
11
|
+
* The SGR numbers below are chalk's tables (ansi-styles) as written; naming each would
|
|
12
|
+
* double the file the R8 ceiling measures, so the lint's magic-number rule is off here.
|
|
13
|
+
*/
|
|
14
|
+
import { type ColorLevel } from './policy.js';
|
|
15
|
+
/** Colour support: none, 16 colours, 256 colours, truecolor. chalk's name for the policy's level. */
|
|
16
|
+
export type ColorSupportLevel = ColorLevel;
|
|
17
|
+
export interface ColorSupport {
|
|
18
|
+
level: ColorSupportLevel;
|
|
19
|
+
hasBasic: boolean;
|
|
20
|
+
has256: boolean;
|
|
21
|
+
has16m: boolean;
|
|
22
|
+
}
|
|
23
|
+
export type ColorInfo = ColorSupport | false;
|
|
24
|
+
export interface ChalkOptions {
|
|
25
|
+
/** `undefined` asks for the level to be detected, as omitting it does. */
|
|
26
|
+
readonly level?: ColorSupportLevel | undefined;
|
|
27
|
+
}
|
|
28
|
+
declare const MODIFIERS: {
|
|
29
|
+
reset: [number, number];
|
|
30
|
+
bold: [number, number];
|
|
31
|
+
dim: [number, number];
|
|
32
|
+
italic: [number, number];
|
|
33
|
+
underline: [number, number];
|
|
34
|
+
underlineDouble: [string, number];
|
|
35
|
+
underlineCurly: [string, number];
|
|
36
|
+
underlineDotted: [string, number];
|
|
37
|
+
underlineDashed: [string, number];
|
|
38
|
+
overline: [number, number];
|
|
39
|
+
inverse: [number, number];
|
|
40
|
+
hidden: [number, number];
|
|
41
|
+
strikethrough: [number, number];
|
|
42
|
+
};
|
|
43
|
+
type Basic = 'black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white';
|
|
44
|
+
type Bright = `${Basic}Bright` | 'gray' | 'grey';
|
|
45
|
+
export type ModifierName = keyof typeof MODIFIERS;
|
|
46
|
+
export type ForegroundColorName = Basic | Bright;
|
|
47
|
+
export type BackgroundColorName = `bg${Capitalize<Basic | Bright>}`;
|
|
48
|
+
export type UnderlineColorName = `underline${Capitalize<Basic | Bright>}`;
|
|
49
|
+
export type ColorName = ForegroundColorName | BackgroundColorName;
|
|
50
|
+
export declare const modifierNames: ModifierName[];
|
|
51
|
+
export declare const foregroundColorNames: ForegroundColorName[];
|
|
52
|
+
export declare const backgroundColorNames: BackgroundColorName[];
|
|
53
|
+
export declare const underlineColorNames: UnderlineColorName[];
|
|
54
|
+
export declare const colorNames: ColorName[];
|
|
55
|
+
interface Base {
|
|
56
|
+
(...text: unknown[]): string;
|
|
57
|
+
/** Mutable, as chalk's is; every builder in a chain reads and writes the instance it came from. */
|
|
58
|
+
level: ColorSupportLevel;
|
|
59
|
+
rgb(red: number, green: number, blue: number): ChalkInstance;
|
|
60
|
+
hex(color: string): ChalkInstance;
|
|
61
|
+
ansi256(index: number): ChalkInstance;
|
|
62
|
+
bgRgb(red: number, green: number, blue: number): ChalkInstance;
|
|
63
|
+
bgHex(color: string): ChalkInstance;
|
|
64
|
+
bgAnsi256(index: number): ChalkInstance;
|
|
65
|
+
underlineRgb(red: number, green: number, blue: number): ChalkInstance;
|
|
66
|
+
underlineHex(color: string): ChalkInstance;
|
|
67
|
+
underlineAnsi256(index: number): ChalkInstance;
|
|
68
|
+
}
|
|
69
|
+
export type ChalkInstance = Base & {
|
|
70
|
+
readonly [K in ModifierName | ColorName | UnderlineColorName | 'visible']: ChalkInstance;
|
|
71
|
+
};
|
|
72
|
+
/** `new Chalk({ level })` — an instance with its own level, detected when the option is omitted. */
|
|
73
|
+
export interface Chalk extends ChalkInstance {
|
|
74
|
+
}
|
|
75
|
+
export declare class Chalk {
|
|
76
|
+
constructor(options?: ChalkOptions);
|
|
77
|
+
}
|
|
78
|
+
export declare const supportsColor: ColorInfo;
|
|
79
|
+
export declare const supportsColorStderr: ColorInfo;
|
|
80
|
+
export declare const chalkStderr: ChalkInstance;
|
|
81
|
+
declare const chalk: ChalkInstance;
|
|
82
|
+
export default chalk;
|
package/dist/chalk.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { colorLevel } from './policy.js';
|
|
2
|
+
import { sgr } from './tokens.js';
|
|
3
|
+
const MODIFIERS = {
|
|
4
|
+
reset: [0, 0],
|
|
5
|
+
bold: [1, 22],
|
|
6
|
+
dim: [2, 22],
|
|
7
|
+
italic: [3, 23],
|
|
8
|
+
underline: [4, 24],
|
|
9
|
+
underlineDouble: ['4:2', 24],
|
|
10
|
+
underlineCurly: ['4:3', 24],
|
|
11
|
+
underlineDotted: ['4:4', 24],
|
|
12
|
+
underlineDashed: ['4:5', 24],
|
|
13
|
+
overline: [53, 55],
|
|
14
|
+
inverse: [7, 27],
|
|
15
|
+
hidden: [8, 28],
|
|
16
|
+
strikethrough: [9, 29],
|
|
17
|
+
};
|
|
18
|
+
const NAMES = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'];
|
|
19
|
+
const cap = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
20
|
+
const pair = (open, close) => ({ open: String(open), close: String(close) });
|
|
21
|
+
function family(prefix, open, close) {
|
|
22
|
+
const t = {};
|
|
23
|
+
const key = (name) => (prefix === '' ? name : prefix + cap(name));
|
|
24
|
+
for (const bright of [false, true]) {
|
|
25
|
+
NAMES.forEach((name, i) => {
|
|
26
|
+
t[key(name) + (bright ? 'Bright' : '')] = pair(open(i, bright), close);
|
|
27
|
+
if (bright && i === 0)
|
|
28
|
+
for (const alias of ['gray', 'grey'])
|
|
29
|
+
t[key(alias)] = pair(open(0, true), close);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return t;
|
|
33
|
+
}
|
|
34
|
+
const FOREGROUND = family('', (i, bright) => (bright ? 90 : 30) + i, 39);
|
|
35
|
+
const BACKGROUND = family('bg', (i, bright) => (bright ? 100 : 40) + i, 49);
|
|
36
|
+
const UNDERLINE = family('underline', (i, bright) => `58;5;${bright ? i + 8 : i}`, 59);
|
|
37
|
+
const STYLES = {
|
|
38
|
+
...Object.fromEntries(Object.entries(MODIFIERS).map(([k, [open, close]]) => [k, pair(open, close)])),
|
|
39
|
+
...FOREGROUND,
|
|
40
|
+
...BACKGROUND,
|
|
41
|
+
...UNDERLINE,
|
|
42
|
+
};
|
|
43
|
+
export const modifierNames = Object.keys(MODIFIERS);
|
|
44
|
+
export const foregroundColorNames = Object.keys(FOREGROUND);
|
|
45
|
+
export const backgroundColorNames = Object.keys(BACKGROUND);
|
|
46
|
+
export const underlineColorNames = Object.keys(UNDERLINE);
|
|
47
|
+
export const colorNames = [...foregroundColorNames, ...backgroundColorNames];
|
|
48
|
+
const step = (v) => Math.round((v / 255) * 5);
|
|
49
|
+
const ramp = (v) => (v < 8 ? 16 : v > 248 ? 231 : Math.round(((v - 8) / 247) * 24) + 232);
|
|
50
|
+
const rgbToAnsi256 = (r, g, b) => r === g && g === b ? ramp(r) : 16 + 36 * step(r) + 6 * step(g) + step(b);
|
|
51
|
+
function ansi256ToAnsi(code) {
|
|
52
|
+
if (code < 16)
|
|
53
|
+
return (code < 8 ? 30 : 82) + code;
|
|
54
|
+
const c = code - 16;
|
|
55
|
+
const grey = ((code - 232) * 10 + 8) / 255;
|
|
56
|
+
const [r, g, b] = code >= 232 ? [grey, grey, grey] : [Math.floor(c / 36) / 5, Math.floor((c % 36) / 6) / 5, (c % 6) / 5];
|
|
57
|
+
const value = Math.max(r, g, b) * 2;
|
|
58
|
+
return value === 0 ? 30 : (value === 2 ? 90 : 30) + ((Math.round(b) << 2) | (Math.round(g) << 1) | Math.round(r));
|
|
59
|
+
}
|
|
60
|
+
function hexToRgb(hex) {
|
|
61
|
+
const m = /[\da-f]{6}|[\da-f]{3}/i.exec(hex)?.[0] ?? '0';
|
|
62
|
+
const n = Number.parseInt(m.length === 3 ? [...m].map((c) => c + c).join('') : m, 16);
|
|
63
|
+
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
|
|
64
|
+
}
|
|
65
|
+
const FAMILIES = {
|
|
66
|
+
'': { ext: 38, ansi: String, close: '39' },
|
|
67
|
+
bg: { ext: 48, ansi: (code) => String(code + 10), close: '49' },
|
|
68
|
+
underline: { ext: 58, ansi: (code) => `58;5;${code < 90 ? code - 30 : code - 82}`, close: '59' },
|
|
69
|
+
};
|
|
70
|
+
const MODELS = {};
|
|
71
|
+
for (const [prefix, fam] of Object.entries(FAMILIES)) {
|
|
72
|
+
for (const model of ['rgb', 'hex', 'ansi256'])
|
|
73
|
+
MODELS[prefix === '' ? model : prefix + cap(model)] = [fam, model];
|
|
74
|
+
}
|
|
75
|
+
function open(fam, level, model, args) {
|
|
76
|
+
const rgb = model === 'ansi256' ? undefined : model === 'hex' ? hexToRgb(String(args[0])) : args.map(Number);
|
|
77
|
+
const code = rgb === undefined ? Number(args[0]) : rgbToAnsi256(...rgb);
|
|
78
|
+
return level === 3 && rgb !== undefined ? `${fam.ext};2;${rgb.join(';')}`
|
|
79
|
+
: level >= 2 ? `${fam.ext};5;${code}`
|
|
80
|
+
: fam.ansi(ansi256ToAnsi(code));
|
|
81
|
+
}
|
|
82
|
+
function checkLevel(level) {
|
|
83
|
+
if (!Number.isSafeInteger(level) || level < 0 || level > 3)
|
|
84
|
+
throw new Error('The `level` should be an integer from 0 to 3');
|
|
85
|
+
}
|
|
86
|
+
function link(root, chain, visible, key) {
|
|
87
|
+
const style = STYLES[key];
|
|
88
|
+
if (style !== undefined)
|
|
89
|
+
return builder(root, [...chain, style], visible);
|
|
90
|
+
if (key === 'visible')
|
|
91
|
+
return builder(root, chain, true);
|
|
92
|
+
const model = MODELS[key];
|
|
93
|
+
if (model === undefined)
|
|
94
|
+
return undefined;
|
|
95
|
+
const [fam, name] = model;
|
|
96
|
+
return (...args) => builder(root, [...chain, { open: open(fam, root.level, name, args), close: fam.close }], visible);
|
|
97
|
+
}
|
|
98
|
+
function builder(root, chain, visible) {
|
|
99
|
+
const links = new Map();
|
|
100
|
+
const fn = (...text) => {
|
|
101
|
+
const s = text.length === 1 ? String(text[0]) : text.join(' ');
|
|
102
|
+
return root.level === 0 || s === '' ? (visible ? '' : s) : chain.length === 0 ? s : sgr(chain, s);
|
|
103
|
+
};
|
|
104
|
+
return new Proxy(fn, {
|
|
105
|
+
get(target, key) {
|
|
106
|
+
if (key === 'level')
|
|
107
|
+
return root.level;
|
|
108
|
+
if (typeof key !== 'string')
|
|
109
|
+
return Reflect.get(target, key);
|
|
110
|
+
if (!links.has(key))
|
|
111
|
+
links.set(key, link(root, chain, visible, key));
|
|
112
|
+
return links.get(key) ?? Reflect.get(target, key);
|
|
113
|
+
},
|
|
114
|
+
set(target, key, value) {
|
|
115
|
+
if (key !== 'level')
|
|
116
|
+
return Reflect.set(target, key, value);
|
|
117
|
+
checkLevel(value);
|
|
118
|
+
root.level = value;
|
|
119
|
+
return true;
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const proc = globalThis
|
|
124
|
+
.process;
|
|
125
|
+
const detect = (stream) => colorLevel({ env: proc?.env ?? {}, argv: proc?.argv ?? [], isTTY: { stdout: proc?.[stream]?.isTTY === true } });
|
|
126
|
+
const stdoutLevel = detect('stdout');
|
|
127
|
+
const stderrLevel = detect('stderr');
|
|
128
|
+
const info = (level) => (level === 0 ? false : { level, hasBasic: true, has256: level >= 2, has16m: level === 3 });
|
|
129
|
+
function create(options = {}, detected = stdoutLevel) {
|
|
130
|
+
if (options.level !== undefined)
|
|
131
|
+
checkLevel(options.level);
|
|
132
|
+
return builder({ level: options.level ?? detected }, [], false);
|
|
133
|
+
}
|
|
134
|
+
export class Chalk {
|
|
135
|
+
constructor(options) {
|
|
136
|
+
return create(options);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export const supportsColor = info(stdoutLevel);
|
|
140
|
+
export const supportsColorStderr = info(stderrLevel);
|
|
141
|
+
export const chalkStderr = create({}, stderrLevel);
|
|
142
|
+
const chalk = create();
|
|
143
|
+
export default chalk;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The WCAG 2.2 contrast maths (R5), copied from `burgee/contrast` on purpose: sixty lines
|
|
3
|
+
* duplicated beats a dependency arrow pointing the wrong way (U1). This copy is held to
|
|
4
|
+
* `contrast-vectors.json` by its test; burgee's test does not read that file yet, so the
|
|
5
|
+
* drift lock is one-sided until the follow-up lands (burgee contrast test to read roundel's
|
|
6
|
+
* contrast-vectors.json).
|
|
7
|
+
*
|
|
8
|
+
* `fly()` uses it to refuse a truecolor token that would not read against the declared
|
|
9
|
+
* ground. Nothing here is asked about the 16- and 256-colour palettes: those are the
|
|
10
|
+
* user's terminal theme, and a number there would be invented.
|
|
11
|
+
*/
|
|
12
|
+
/** The floors WCAG 2.2 sets, as ratios. */
|
|
13
|
+
export declare const AA: {
|
|
14
|
+
/** Body text against its background. */
|
|
15
|
+
readonly TEXT: 4.5;
|
|
16
|
+
/** Large text, UI components, and meaningful parts of a graphic. */
|
|
17
|
+
readonly GRAPHIC: 3;
|
|
18
|
+
};
|
|
19
|
+
/** `#abc` and `#aabbcc` both parse, to sRGB channels in 0..1. Anything else is a mistake worth throwing on. */
|
|
20
|
+
export declare function channels(hex: string): [number, number, number];
|
|
21
|
+
/** WCAG relative luminance. */
|
|
22
|
+
export declare function luminance(hex: string): number;
|
|
23
|
+
/** The WCAG contrast ratio between two colours. Order does not matter. */
|
|
24
|
+
export declare function contrast(a: string, b: string): number;
|
package/dist/contrast.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const AA = {
|
|
2
|
+
TEXT: 4.5,
|
|
3
|
+
GRAPHIC: 3,
|
|
4
|
+
};
|
|
5
|
+
const SRGB_MAX = 255;
|
|
6
|
+
const LINEAR_THRESHOLD = 0.03928;
|
|
7
|
+
const LINEAR_DIVISOR = 12.92;
|
|
8
|
+
const GAMMA_OFFSET = 0.055;
|
|
9
|
+
const GAMMA_SCALE = 1.055;
|
|
10
|
+
const GAMMA_EXPONENT = 2.4;
|
|
11
|
+
const LUMA = { r: 0.2126, g: 0.7152, b: 0.0722 };
|
|
12
|
+
const CONTRAST_OFFSET = 0.05;
|
|
13
|
+
const RED_AT = 1;
|
|
14
|
+
const GREEN_AT = 3;
|
|
15
|
+
const BLUE_AT = 5;
|
|
16
|
+
const HEX_PAIRS = [RED_AT, GREEN_AT, BLUE_AT];
|
|
17
|
+
const HEX_RADIX = 16;
|
|
18
|
+
const SHORT_HEX_LENGTH = 4;
|
|
19
|
+
export function channels(hex) {
|
|
20
|
+
const full = hex.length === SHORT_HEX_LENGTH
|
|
21
|
+
? `#${hex[1]}${hex[1]}${hex[2]}${hex[2]}${hex[3]}${hex[3]}`
|
|
22
|
+
: hex;
|
|
23
|
+
if (!/^#[0-9a-fA-F]{6}$/.test(full))
|
|
24
|
+
throw new Error(`roundel: "${hex}" is not a hex colour`);
|
|
25
|
+
const parsed = HEX_PAIRS.map((i) => Number.parseInt(full.slice(i, i + 2), HEX_RADIX) / SRGB_MAX);
|
|
26
|
+
return parsed;
|
|
27
|
+
}
|
|
28
|
+
export function luminance(hex) {
|
|
29
|
+
const [r, g, b] = channels(hex).map((v) => v <= LINEAR_THRESHOLD ? v / LINEAR_DIVISOR : ((v + GAMMA_OFFSET) / GAMMA_SCALE) ** GAMMA_EXPONENT);
|
|
30
|
+
return LUMA.r * r + LUMA.g * g + LUMA.b * b;
|
|
31
|
+
}
|
|
32
|
+
export function contrast(a, b) {
|
|
33
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
34
|
+
return (hi + CONTRAST_OFFSET) / (lo + CONTRAST_OFFSET);
|
|
35
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* roundel —
|
|
3
|
-
*
|
|
4
|
-
* exports only its own name, so that importing it costs nothing and promises nothing.
|
|
2
|
+
* roundel — the colours a CLI carries. Re-exports only; each subpath is its own entry
|
|
3
|
+
* and costs only itself (R7), so prefer `roundel/tokens` when that is all you need.
|
|
5
4
|
*/
|
|
6
|
-
export
|
|
5
|
+
export * from './contrast.js';
|
|
6
|
+
export * from './plugin.js';
|
|
7
|
+
export * from './policy.js';
|
|
8
|
+
export * from './theme.js';
|
|
9
|
+
export * from './tokens.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
export const name = 'roundel';
|
|
7
|
-
//# sourceMappingURL=index.js.map
|
|
1
|
+
export * from './contrast.js';
|
|
2
|
+
export * from './plugin.js';
|
|
3
|
+
export * from './policy.js';
|
|
4
|
+
export * from './theme.js';
|
|
5
|
+
export * from './tokens.js';
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin host for roundel's half of the contract (`plugin-contract` R1, R4, R6, R8).
|
|
3
|
+
*
|
|
4
|
+
* A plugin is one plain object shared by the whole family. This file keeps the key roundel
|
|
5
|
+
* understands — `tokens`, a theme — and **ignores every other key without complaining**,
|
|
6
|
+
* which is what makes the same object work on any subset of the family that is installed.
|
|
7
|
+
* A plugin written for flagstaff registers here and contributes its theme; its `spinners`
|
|
8
|
+
* and `components` are not roundel's business and are not an error.
|
|
9
|
+
*
|
|
10
|
+
* **Nothing here imports flagstaff, and the shape is declared rather than imported.** Types
|
|
11
|
+
* erase, so an import would cost nothing at run time — and it would still put flagstaff in
|
|
12
|
+
* roundel's dependency story, which is the one thing the family promises it does not do.
|
|
13
|
+
* Four keys declared structurally is three lines, and keeps "none requires the others"
|
|
14
|
+
* literally true instead of true-modulo-types.
|
|
15
|
+
*
|
|
16
|
+
* **A plugin cannot smuggle an unreadable colour in.** This file collects tokens; `fly()`
|
|
17
|
+
* contrast-checks them against the ground exactly as it checks a hand-written theme, and
|
|
18
|
+
* throws below 4.5:1. That is deliberately not re-implemented here: one contrast gate, in
|
|
19
|
+
* the place that already had it (roundel R5).
|
|
20
|
+
*/
|
|
21
|
+
import { type Hex, type Theme } from './theme.js';
|
|
22
|
+
/**
|
|
23
|
+
* The plugin contract version. One number for the family — the same `1` flagstaff declares,
|
|
24
|
+
* written out rather than imported for the reason in the file comment above.
|
|
25
|
+
*/
|
|
26
|
+
export declare const CONTRACT = 1;
|
|
27
|
+
/**
|
|
28
|
+
* The keys roundel reads. Declared structurally: any object with these fields is a plugin
|
|
29
|
+
* here, whatever else it carries.
|
|
30
|
+
*/
|
|
31
|
+
export interface Plugin {
|
|
32
|
+
name: string;
|
|
33
|
+
contract?: number;
|
|
34
|
+
tokens?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
export type PluginErrorCode = 'E_PLUGIN_SCHEMA' | 'E_PLUGIN_CONTRACT';
|
|
37
|
+
/** A refused plugin says what is wrong and what to do about it — the family's one vocabulary. */
|
|
38
|
+
export declare class PluginError extends Error {
|
|
39
|
+
readonly code: PluginErrorCode;
|
|
40
|
+
readonly fix: string;
|
|
41
|
+
constructor(code: PluginErrorCode, message: string, fix: string);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Refuse a plugin that cannot contribute a theme, at the door.
|
|
45
|
+
*
|
|
46
|
+
* A misspelt token name is refused rather than ignored: a plugin whose `errror` key is
|
|
47
|
+
* silently dropped looks like it worked, and the author debugs the wrong thing.
|
|
48
|
+
*/
|
|
49
|
+
export declare function validate(plugin: unknown): asserts plugin is Plugin;
|
|
50
|
+
/** Which plugin last contributed each token — the shadowing a `plugin check` prints. */
|
|
51
|
+
export interface Contribution {
|
|
52
|
+
token: string;
|
|
53
|
+
value: Hex;
|
|
54
|
+
from: string;
|
|
55
|
+
/** Plugins that contributed this token earlier and were overridden, in order. */
|
|
56
|
+
shadowed: string[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Register a plugin. Later wins, like ESLint flat config: the array is ordered, a caller
|
|
60
|
+
* reads it top to bottom, and the last word on a token is the one nearest the program.
|
|
61
|
+
*/
|
|
62
|
+
export declare function register(plugin: unknown): void;
|
|
63
|
+
/** Forget every registered plugin. For tests, and for a program that re-themes at runtime. */
|
|
64
|
+
export declare function reset(): void;
|
|
65
|
+
/**
|
|
66
|
+
* The theme every registered plugin adds up to, ready for `fly()`.
|
|
67
|
+
*
|
|
68
|
+
* It is *not* flown here. A plugin contributing colour must not decide when colour is
|
|
69
|
+
* decided — `fly()` is called once by the program, and calling it from a `register()` would
|
|
70
|
+
* mean the last plugin imported quietly re-flew the theme.
|
|
71
|
+
*/
|
|
72
|
+
export declare function theme(): Theme;
|
|
73
|
+
/** Every token a plugin contributed, with who won it and who it shadowed. */
|
|
74
|
+
export declare function contributions(): Contribution[];
|
|
75
|
+
/** The plugins registered, in registration order. */
|
|
76
|
+
export declare function registered(): readonly Plugin[];
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const CONTRACT = 1;
|
|
2
|
+
export class PluginError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
fix;
|
|
5
|
+
constructor(code, message, fix) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.fix = fix;
|
|
9
|
+
this.name = 'PluginError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const TOKEN_NAMES = new Set(['error', 'warn', 'ok', 'hint', 'muted', 'command', 'flag', 'value', 'heading', 'ground']);
|
|
13
|
+
const HEX = /^#[0-9a-f]{6}$/i;
|
|
14
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
15
|
+
export function validate(plugin) {
|
|
16
|
+
if (!isRecord(plugin))
|
|
17
|
+
throw new PluginError('E_PLUGIN_SCHEMA', 'a plugin is a plain object', 'export an object, not a function or an array');
|
|
18
|
+
if (typeof plugin['name'] !== 'string' || plugin['name'] === '') {
|
|
19
|
+
throw new PluginError('E_PLUGIN_SCHEMA', 'a plugin needs a name', 'add `name: "…"` — it is how a shadowed token is reported');
|
|
20
|
+
}
|
|
21
|
+
const contract = plugin['contract'];
|
|
22
|
+
if (contract !== undefined && (!Number.isInteger(contract) || contract > CONTRACT)) {
|
|
23
|
+
throw new PluginError('E_PLUGIN_CONTRACT', `plugin "${plugin['name']}" declares contract ${String(contract)}; this roundel knows ${CONTRACT}`, 'upgrade roundel, or lower the plugin’s contract');
|
|
24
|
+
}
|
|
25
|
+
validateTokens(plugin['tokens'], plugin['name']);
|
|
26
|
+
}
|
|
27
|
+
function validateTokens(tokens, name) {
|
|
28
|
+
if (tokens === undefined)
|
|
29
|
+
return;
|
|
30
|
+
if (!isRecord(tokens))
|
|
31
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": tokens must be an object`, 'map a token name to a #rrggbb colour');
|
|
32
|
+
for (const [token, value] of Object.entries(tokens)) {
|
|
33
|
+
if (!TOKEN_NAMES.has(token)) {
|
|
34
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": "${token}" is not a token`, `use one of ${[...TOKEN_NAMES].join(', ')}`);
|
|
35
|
+
}
|
|
36
|
+
if (typeof value !== 'string' || !HEX.test(value)) {
|
|
37
|
+
throw new PluginError('E_PLUGIN_SCHEMA', `plugin "${name}": token "${token}" is ${JSON.stringify(value)}`, 'a token is a #rrggbb colour, contrast-checked when the theme is flown');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const order = [];
|
|
42
|
+
export function register(plugin) {
|
|
43
|
+
validate(plugin);
|
|
44
|
+
order.push(plugin);
|
|
45
|
+
}
|
|
46
|
+
export function reset() {
|
|
47
|
+
order.length = 0;
|
|
48
|
+
}
|
|
49
|
+
export function theme() {
|
|
50
|
+
const out = new Map();
|
|
51
|
+
for (const plugin of order) {
|
|
52
|
+
for (const [token, value] of Object.entries(plugin.tokens ?? {})) {
|
|
53
|
+
if (TOKEN_NAMES.has(token))
|
|
54
|
+
out.set(token, value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return Object.fromEntries(out);
|
|
58
|
+
}
|
|
59
|
+
export function contributions() {
|
|
60
|
+
const by = new Map();
|
|
61
|
+
for (const plugin of order) {
|
|
62
|
+
for (const [token, value] of Object.entries(plugin.tokens ?? {})) {
|
|
63
|
+
const existing = by.get(token);
|
|
64
|
+
by.set(token, { token, value: value, from: plugin.name, shadowed: existing === undefined ? [] : [...existing.shadowed, existing.from] });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [...by.values()];
|
|
68
|
+
}
|
|
69
|
+
export function registered() {
|
|
70
|
+
return order;
|
|
71
|
+
}
|
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The output policy — one answer to "where is this output going?", read from a
|
|
3
|
+
* `Runtime`, never from `process` (R1, R2, R9 of `roundel`; U2 of the stack).
|
|
4
|
+
*
|
|
5
|
+
* Five colour libraries disagreeing about the terminal (clack #286) is what this file
|
|
6
|
+
* exists to end: every package in the family asks here, and nothing else in it reads
|
|
7
|
+
* `isTTY`, `NO_COLOR`, `FORCE_COLOR`, `CI`, `CLI_ACCESSIBLE` or a `--color` flag. It also
|
|
8
|
+
* holds the one record `fly()` writes and every token reads, because it is the only module
|
|
9
|
+
* every subpath may import (R7).
|
|
10
|
+
*
|
|
11
|
+
* The code here is written tight — ternaries where a reader might expect statements —
|
|
12
|
+
* because `./chalk` reaches this file and R8 caps that whole graph at chalk 6's own
|
|
13
|
+
* 9,370 bytes. Comments are stripped from `dist`, so the prose is free; the statements
|
|
14
|
+
* are not.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* The slice of a runtime the policy needs. burgee's `processRuntime` satisfies it, so
|
|
18
|
+
* does a two-line literal in a test; nothing here imports a type from anywhere.
|
|
19
|
+
*/
|
|
20
|
+
export interface Runtime {
|
|
21
|
+
env: Record<string, string | undefined>;
|
|
22
|
+
isTTY: {
|
|
23
|
+
stdout: boolean;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* The process arguments, when the caller owns them: `--color`, `--no-color` and
|
|
27
|
+
* `--color=…` are read here and nowhere else. A test literal leaves it out.
|
|
28
|
+
*/
|
|
29
|
+
argv?: readonly string[];
|
|
30
|
+
}
|
|
31
|
+
export type OutputMode = 'tty' | 'pipe' | 'json' | 'accessible' | 'ci';
|
|
32
|
+
declare const MAX_LEVEL = 3;
|
|
33
|
+
/** chalk's levels: none, 16 colours, 256 colours, truecolor. */
|
|
34
|
+
export type ColorLevel = 0 | 1 | 2 | typeof MAX_LEVEL;
|
|
35
|
+
export interface ModeOptions {
|
|
36
|
+
/** Whether this run was asked for `--json`: the engine's knowledge, not the process's. */
|
|
37
|
+
json?: boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* First match wins: `json` if asked; `accessible` if `CLI_ACCESSIBLE`; `ci` if `CI` and not
|
|
41
|
+
* a TTY; `pipe` if not a TTY; else `tty`.
|
|
42
|
+
*/
|
|
43
|
+
export declare const outputMode: (rt: Runtime, opts?: ModeOptions) => OutputMode;
|
|
44
|
+
/**
|
|
45
|
+
* chalk's level, obeying the user's explicit instruction in any mode (R2 revised
|
|
46
|
+
* 2026-09-08): `NO_COLOR` wins outright; then `FORCE_COLOR=0`, which supports-color settles
|
|
47
|
+
* *before* it looks at any flag, so `FORCE_COLOR=0 --color=256` is off and not 2 — an
|
|
48
|
+
* explicit "colour off" is never undone by a level flag; then the `--color` flags in
|
|
49
|
+
* `argv`, an exact `--color=256` beating a numeric `FORCE_COLOR` as chalk's own suite says;
|
|
50
|
+
* then `FORCE_COLOR`. `--json` is the one output the level never enters: structured text
|
|
51
|
+
* carries no escapes. The mode decides redraws (U2), never the level.
|
|
52
|
+
*
|
|
53
|
+
* Accessible mode defaults to 0, on the same footing as a pipe rather than a terminal:
|
|
54
|
+
* `CLI_ACCESSIBLE` is itself an explicit instruction from a human, and ANSI colour is noise
|
|
55
|
+
* to a screen reader. As with a pipe, an explicit colour ask (`FORCE_COLOR`, `--color=…`)
|
|
56
|
+
* still wins and `NO_COLOR` still beats everything.
|
|
57
|
+
*
|
|
58
|
+
* With no instruction the order is supports-color's own, and deliberately so — chalk's
|
|
59
|
+
* `level.js` asserts it, and a family that disagreed with chalk about a bare pipe would be
|
|
60
|
+
* the clack #286 bug again. A pipe is 0, because a pipe nobody asked to colour is a file or
|
|
61
|
+
* another program's stdin; the one exception is Azure Pipelines (`TF_BUILD` *and*
|
|
62
|
+
* `AGENT_NAME` — `TF_BUILD` alone is a build without an agent), which supports-color reads
|
|
63
|
+
* before it gives up on a pipe. Once colour *is* being detected — a terminal, or a run that
|
|
64
|
+
* asked — `TERM=dumb` is the floor, a `CI` run is its vendor's level (gated on `'CI' in env`
|
|
65
|
+
* as supports-color gates it, so an empty `CI=` still selects the table), and anything else
|
|
66
|
+
* is what `TERM`/`COLORTERM` report. So the CI user who exports `FORCE_COLOR=true` to get
|
|
67
|
+
* coloured logs gets their runner's colours, and nobody else's pipe changes.
|
|
68
|
+
*
|
|
69
|
+
* ponytail: supports-color also consults the platform, TEAMCITY_VERSION, TERM_PROGRAM and
|
|
70
|
+
* the emulator allow-list; R2 refuses that detection, so those stay unread.
|
|
71
|
+
*/
|
|
72
|
+
export declare function colorLevel(rt: Runtime, opts?: ModeOptions): ColorLevel;
|
|
73
|
+
export type TokenName = 'error' | 'warn' | 'ok' | 'hint' | 'muted' | 'command' | 'flag' | 'value' | 'heading';
|
|
74
|
+
/** One `util.styleText` format name: `'bold'`, `'red'`, `'redBright'`, `'dim'`… */
|
|
75
|
+
export type Format = import('node:util').InspectColor;
|
|
76
|
+
/**
|
|
77
|
+
* A style as a token paints it: `styleText` format names, or the SGR parameters of one
|
|
78
|
+
* foreground colour (`38;5;n` for 256 colours, `38;2;r;g;b` for truecolor), which
|
|
79
|
+
* `styleText` cannot express.
|
|
80
|
+
*/
|
|
81
|
+
export type Paint = readonly Format[] | {
|
|
82
|
+
readonly sgr: readonly number[];
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* What `fly()` decided, once, for the process: the colour level and each token's paint.
|
|
86
|
+
* Every token reads it; nothing writes it but `fly()`. Until then the level is 0 and every
|
|
87
|
+
* token is the identity, so a program that never declares its runtime prints plain text.
|
|
88
|
+
*/
|
|
89
|
+
export declare const flown: {
|
|
90
|
+
level: ColorLevel;
|
|
91
|
+
paint: Partial<Record<TokenName, Paint>>;
|
|
92
|
+
};
|
|
93
|
+
export {};
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const MAX_LEVEL = 3;
|
|
2
|
+
const set = (value) => value !== undefined && value !== '';
|
|
3
|
+
export const outputMode = (rt, opts = {}) => opts.json === true ? 'json' : set(rt.env['CLI_ACCESSIBLE']) ? 'accessible' : rt.isTTY.stdout ? 'tty' : set(rt.env['CI']) ? 'ci' : 'pipe';
|
|
4
|
+
const forced = (v) => v === 'false' ? 0 : v === '' || v === 'true' ? 'on' : v !== undefined && /^\d+$/.test(v) ? Math.min(Number(v), MAX_LEVEL) : undefined;
|
|
5
|
+
function flagged(argv) {
|
|
6
|
+
const end = argv.indexOf('--');
|
|
7
|
+
const seen = end < 0 ? argv : argv.slice(0, end);
|
|
8
|
+
const has = (re) => seen.some((a) => re.test(a));
|
|
9
|
+
return has(/^--(no-colors?|color=(false|never))$/) ? 0
|
|
10
|
+
: has(/^--color=(16m|full|truecolor)$/) ? MAX_LEVEL
|
|
11
|
+
: has(/^--color=256$/) ? 2
|
|
12
|
+
: has(/^--(colors|color(=(true|always))?)$/) ? 'on'
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
15
|
+
const CI_3 = ['GITHUB_ACTIONS', 'GITEA_ACTIONS', 'CIRCLECI'];
|
|
16
|
+
const CI_1 = ['TRAVIS', 'APPVEYOR', 'GITLAB_CI', 'BUILDKITE', 'DRONE'];
|
|
17
|
+
const ci = (env) => CI_3.some((k) => k in env) ? MAX_LEVEL : CI_1.some((k) => k in env) || env['CI_NAME'] === 'codeship' ? 1 : 0;
|
|
18
|
+
const TERM_256 = /-256(color)?$/i;
|
|
19
|
+
const TERM_16 = /^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i;
|
|
20
|
+
const terminal = ({ TERM = '', COLORTERM }) => COLORTERM === 'truecolor' ? MAX_LEVEL : TERM_256.test(TERM) ? 2 : TERM_16.test(TERM) || COLORTERM !== undefined ? 1 : 0;
|
|
21
|
+
export function colorLevel(rt, opts) {
|
|
22
|
+
const { env } = rt;
|
|
23
|
+
const force = forced(env['FORCE_COLOR']);
|
|
24
|
+
const ask = force === 0 ? 0 : (flagged(rt.argv ?? []) ?? force);
|
|
25
|
+
const min = ask === undefined ? 0 : 1;
|
|
26
|
+
return opts?.json === true || set(env['NO_COLOR']) || (min === 0 && set(env['CLI_ACCESSIBLE'])) ? 0 : typeof ask === 'number' ? ask : 'TF_BUILD' in env && 'AGENT_NAME' in env ? 1 : min === 0 && !rt.isTTY.stdout ? 0 : env['TERM'] === 'dumb' ? min : Math.max(min, 'CI' in env ? ci(env) : terminal(env));
|
|
27
|
+
}
|
|
28
|
+
export const flown = { level: 0, paint: {} };
|
package/dist/schema.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://github.com/ofri-peretz/burgee/blob/main/packages/flagstaff/src/schema.json","title":"flagstaff plugin","description":"A plugin is one plain object. Everything in it is data that can be read without running it; the only functions allowed are a component's `static` (required) and `frame` (optional). A spinner or component without a static projection is refused at register().","type":"object","required":["name"],"additionalProperties":true,"properties":{"name":{"type":"string","minLength":1,"description":"The plugin's name; also the prefix a host may use when two plugins contribute the same key."},"contract":{"type":"integer","minimum":1,"description":"The plugin contract this object follows. A host refuses a newer contract than it knows."},"tokens":{"type":"object","description":"A roundel theme: semantic token name to a hex colour, contrast-checked when flown.","additionalProperties":{"type":"string","pattern":"^#[0-9a-fA-F]{6}$"}},"glyphs":{"type":"object","description":"Symbols by meaning: `ok`, `fail`, `warn`, `info`, `running`. A plugin that ships glyphs changes every built-in that draws one.","additionalProperties":{"type":"string","minLength":1}},"spinners":{"type":"object","description":"Spinner styles by name, in cli-spinners' shape plus the static projection.","additionalProperties":{"$ref":"#/$defs/spinner"}},"borders":{"type":"object","description":"Border styles a box can be drawn with, by name.","additionalProperties":{"$ref":"#/$defs/border"}},"components":{"type":"object","description":"Components by name: `static(state)` returns the text a pipe, an agent or a screen reader gets; `frame(t, state)` is the optional animated form.","additionalProperties":{"$ref":"#/$defs/component"}}},"$defs":{"spinner":{"type":"object","required":["frames","interval","static"],"properties":{"frames":{"type":"array","items":{"type":"string"},"minItems":1},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between frames on a terminal."},"static":{"type":"string","description":"What a pipe prints instead of the animation."}}},"component":{"type":"object","required":["static"],"properties":{"static":{"description":"(state) => string. Required: the projection every non-terminal mode prints."},"frame":{"description":"(t, state) => string. Optional: the frame at t milliseconds since hoisting."},"sample":{"type":"object","required":["running","done"],"description":"Two states to *show* this component with: `flagstaff check` and the docs gallery render `running` then `done`. Omitted, they assume `{ phase: 'running' }` and `{ phase: 'done' }` and say so in the output. The loop never reads it — a running program's state comes from the program.","properties":{"running":{"description":"The state to open with."},"done":{"description":"The state to close with."}}},"interval":{"type":"integer","minimum":1,"description":"Milliseconds between repaints when `frame` is given; 80 when omitted."}}},"border":{"type":"object","required":["topLeft","top","topRight","left","right","bottomLeft","bottom","bottomRight"],"description":"cli-boxes' shape exactly, so that corpus imports unchanged.","properties":{"topLeft":{"type":"string"},"top":{"type":"string"},"topRight":{"type":"string"},"left":{"type":"string"},"right":{"type":"string"},"bottomLeft":{"type":"string"},"bottom":{"type":"string"},"bottomRight":{"type":"string"}}}}}
|
package/dist/theme.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type Format, type ModeOptions, type Runtime, type TokenName } from './policy.js';
|
|
2
|
+
export type Hex = `#${string}`;
|
|
3
|
+
export type Style = Hex | readonly Format[];
|
|
4
|
+
/** Each token's style, and the ground the hex ones are checked against (default: near-black). */
|
|
5
|
+
export type Theme = Partial<Record<TokenName, Style>> & {
|
|
6
|
+
ground?: Hex;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Fly the theme: decide the colour level from the runtime once, check every hex token
|
|
10
|
+
* against the ground, and set what the tokens paint from now on. Call it at startup, with
|
|
11
|
+
* `{ json }` when the run was asked for `--json`; a later call replaces the theme.
|
|
12
|
+
*/
|
|
13
|
+
export declare function fly(theme: Theme, rt: Runtime, opts?: ModeOptions): void;
|
package/dist/theme.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { AA, channels, contrast } from './contrast.js';
|
|
2
|
+
import { colorLevel, flown, } from './policy.js';
|
|
3
|
+
const ROCK = ['#a84c17', '#f4794a'];
|
|
4
|
+
const JUNIPER = ['#0a6b47', '#0d9460'];
|
|
5
|
+
const INK = '#0a0a0a';
|
|
6
|
+
function brand(pair, ground) {
|
|
7
|
+
const [deep, lifted] = pair;
|
|
8
|
+
return contrast(lifted, ground) > contrast(deep, ground) ? lifted : deep;
|
|
9
|
+
}
|
|
10
|
+
const DEFAULTS = {
|
|
11
|
+
error: (ground) => brand(ROCK, ground),
|
|
12
|
+
warn: () => ['yellow'],
|
|
13
|
+
ok: (ground) => brand(JUNIPER, ground),
|
|
14
|
+
hint: () => ['dim'],
|
|
15
|
+
muted: () => ['gray'],
|
|
16
|
+
command: () => ['bold'],
|
|
17
|
+
flag: () => ['cyan'],
|
|
18
|
+
value: () => ['magenta'],
|
|
19
|
+
heading: () => ['bold', 'underline'],
|
|
20
|
+
};
|
|
21
|
+
const TOKENS = Object.keys(DEFAULTS);
|
|
22
|
+
const SRGB_MAX = 255;
|
|
23
|
+
const CUBE_STEPS = 5;
|
|
24
|
+
const CUBE_START = 16;
|
|
25
|
+
const CUBE_ROW = 36;
|
|
26
|
+
const CUBE_COL = 6;
|
|
27
|
+
const GREY_START = 232;
|
|
28
|
+
const GREY_STEPS = 24;
|
|
29
|
+
const GREY_LOW = 8;
|
|
30
|
+
const GREY_HIGH = 248;
|
|
31
|
+
const GREY_SPAN = 247;
|
|
32
|
+
const CUBE_WHITE = 231;
|
|
33
|
+
const BASIC_NAMES = ['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white'];
|
|
34
|
+
const BLUE_BIT = 2;
|
|
35
|
+
const GREEN_BIT = 1;
|
|
36
|
+
const SGR_FG = 38;
|
|
37
|
+
const SGR_256 = 5;
|
|
38
|
+
const SGR_RGB = 2;
|
|
39
|
+
const TRUECOLOR = 3;
|
|
40
|
+
const COLORS_256 = 2;
|
|
41
|
+
function ansi256(r, g, b) {
|
|
42
|
+
if (r === g && g === b) {
|
|
43
|
+
if (r < GREY_LOW)
|
|
44
|
+
return CUBE_START;
|
|
45
|
+
if (r > GREY_HIGH)
|
|
46
|
+
return CUBE_WHITE;
|
|
47
|
+
return Math.round(((r - GREY_LOW) / GREY_SPAN) * GREY_STEPS) + GREY_START;
|
|
48
|
+
}
|
|
49
|
+
const step = (v) => Math.round((v / SRGB_MAX) * CUBE_STEPS);
|
|
50
|
+
return CUBE_START + CUBE_ROW * step(r) + CUBE_COL * step(g) + step(b);
|
|
51
|
+
}
|
|
52
|
+
function ansi16(r, g, b) {
|
|
53
|
+
const on = (v) => Math.round(v / SRGB_MAX);
|
|
54
|
+
const index = (on(b) << BLUE_BIT) | (on(g) << GREEN_BIT) | on(r);
|
|
55
|
+
const name = BASIC_NAMES[index] ?? 'white';
|
|
56
|
+
if (index === 0)
|
|
57
|
+
return name;
|
|
58
|
+
if (Math.round((Math.max(r, g, b) / SRGB_MAX) * CUBE_STEPS) !== CUBE_STEPS)
|
|
59
|
+
return name;
|
|
60
|
+
return name === 'black' ? 'gray' : `${name}Bright`;
|
|
61
|
+
}
|
|
62
|
+
function resolve(style, level) {
|
|
63
|
+
if (typeof style !== 'string')
|
|
64
|
+
return style;
|
|
65
|
+
const [r, g, b] = channels(style).map((v) => Math.round(v * SRGB_MAX));
|
|
66
|
+
if (level === TRUECOLOR)
|
|
67
|
+
return { sgr: [SGR_FG, SGR_RGB, r, g, b] };
|
|
68
|
+
return level === COLORS_256 ? { sgr: [SGR_FG, SGR_256, ansi256(r, g, b)] } : [ansi16(r, g, b)];
|
|
69
|
+
}
|
|
70
|
+
export function fly(theme, rt, opts) {
|
|
71
|
+
const level = colorLevel(rt, opts);
|
|
72
|
+
const ground = theme.ground ?? INK;
|
|
73
|
+
const styles = TOKENS.map((name) => [name, theme[name] ?? DEFAULTS[name](ground)]);
|
|
74
|
+
const failures = styles.flatMap(([name, style]) => {
|
|
75
|
+
if (typeof style !== 'string')
|
|
76
|
+
return [];
|
|
77
|
+
const ratio = contrast(style, ground);
|
|
78
|
+
return ratio < AA.TEXT ? [`${name} ${style} on ${ground} is ${ratio.toFixed(2)}:1`] : [];
|
|
79
|
+
});
|
|
80
|
+
if (failures.length > 0)
|
|
81
|
+
throw new Error(`roundel: below ${AA.TEXT}:1 — ${failures.join('; ')}`);
|
|
82
|
+
flown.level = level;
|
|
83
|
+
flown.paint = Object.fromEntries(styles.map(([name, style]) => [name, resolve(style, level)]));
|
|
84
|
+
}
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type Token = (s: string) => string;
|
|
2
|
+
/** One SGR pair as the chalk façade composes them: the parameters that open and close it, no escape. */
|
|
3
|
+
export interface SgrPair {
|
|
4
|
+
readonly open: string;
|
|
5
|
+
readonly close: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Wrap `s` in a chain of SGR pairs, outermost first, as chalk does: a close already inside
|
|
9
|
+
* `s` is followed by a re-open so a nested style survives it, and every line break closes
|
|
10
|
+
* before it and re-opens after (chalk/chalk#92). The façade computes parameters; the
|
|
11
|
+
* escape itself is emitted here and nowhere else (R3, R6).
|
|
12
|
+
*/
|
|
13
|
+
export declare function sgr(chain: readonly SgrPair[], s: string): string;
|
|
14
|
+
export declare const error: Token;
|
|
15
|
+
export declare const warn: Token;
|
|
16
|
+
export declare const ok: Token;
|
|
17
|
+
export declare const hint: Token;
|
|
18
|
+
export declare const muted: Token;
|
|
19
|
+
export declare const command: Token;
|
|
20
|
+
export declare const flag: Token;
|
|
21
|
+
export declare const value: Token;
|
|
22
|
+
export declare const heading: Token;
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { flown } from './policy.js';
|
|
3
|
+
const CSI = '\u001B[';
|
|
4
|
+
const FG_RESET = `${CSI}39m`;
|
|
5
|
+
function paint(p, s) {
|
|
6
|
+
if ('sgr' in p)
|
|
7
|
+
return `${CSI}${p.sgr.join(';')}m${s}${FG_RESET}`;
|
|
8
|
+
return styleText([...p], s, { validateStream: false });
|
|
9
|
+
}
|
|
10
|
+
const LINE_BREAK = /\r?\n/g;
|
|
11
|
+
export function sgr(chain, s) {
|
|
12
|
+
const code = (p) => `${CSI}${p}m`;
|
|
13
|
+
const openAll = chain.map((p) => code(p.open)).join('');
|
|
14
|
+
const closeAll = chain.map((p) => code(p.close)).reverse().join('');
|
|
15
|
+
let out = s;
|
|
16
|
+
if (out.includes(CSI))
|
|
17
|
+
for (const p of chain.toReversed())
|
|
18
|
+
out = out.replaceAll(code(p.close), code(p.close) + code(p.open));
|
|
19
|
+
return openAll + out.replace(LINE_BREAK, (lf) => closeAll + lf + openAll) + closeAll;
|
|
20
|
+
}
|
|
21
|
+
function token(name) {
|
|
22
|
+
return (s) => {
|
|
23
|
+
const p = flown.level === 0 ? undefined : flown.paint[name];
|
|
24
|
+
return p === undefined ? s : paint(p, s);
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export const error = token('error');
|
|
28
|
+
export const warn = token('warn');
|
|
29
|
+
export const ok = token('ok');
|
|
30
|
+
export const hint = token('hint');
|
|
31
|
+
export const muted = token('muted');
|
|
32
|
+
export const command = token('command');
|
|
33
|
+
export const flag = token('flag');
|
|
34
|
+
export const value = token('value');
|
|
35
|
+
export const heading = token('heading');
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roundel",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The colours a CLI carries. One output policy, semantic tokens, a theme, and a chalk migration path lighter than chalk. Zero dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
7
8
|
"engines": {
|
|
8
9
|
"node": ">=24"
|
|
9
10
|
},
|
|
@@ -12,6 +13,37 @@
|
|
|
12
13
|
"types": "./dist/index.d.ts",
|
|
13
14
|
"import": "./dist/index.js",
|
|
14
15
|
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./policy": {
|
|
18
|
+
"types": "./dist/policy.d.ts",
|
|
19
|
+
"import": "./dist/policy.js",
|
|
20
|
+
"default": "./dist/policy.js"
|
|
21
|
+
},
|
|
22
|
+
"./tokens": {
|
|
23
|
+
"types": "./dist/tokens.d.ts",
|
|
24
|
+
"import": "./dist/tokens.js",
|
|
25
|
+
"default": "./dist/tokens.js"
|
|
26
|
+
},
|
|
27
|
+
"./schema.json": "./dist/schema.json",
|
|
28
|
+
"./plugin": {
|
|
29
|
+
"types": "./dist/plugin.d.ts",
|
|
30
|
+
"import": "./dist/plugin.js",
|
|
31
|
+
"default": "./dist/plugin.js"
|
|
32
|
+
},
|
|
33
|
+
"./theme": {
|
|
34
|
+
"types": "./dist/theme.d.ts",
|
|
35
|
+
"import": "./dist/theme.js",
|
|
36
|
+
"default": "./dist/theme.js"
|
|
37
|
+
},
|
|
38
|
+
"./contrast": {
|
|
39
|
+
"types": "./dist/contrast.d.ts",
|
|
40
|
+
"import": "./dist/contrast.js",
|
|
41
|
+
"default": "./dist/contrast.js"
|
|
42
|
+
},
|
|
43
|
+
"./chalk": {
|
|
44
|
+
"types": "./dist/chalk.d.ts",
|
|
45
|
+
"import": "./dist/chalk.js",
|
|
46
|
+
"default": "./dist/chalk.js"
|
|
15
47
|
}
|
|
16
48
|
},
|
|
17
49
|
"files": [
|
|
@@ -20,9 +52,10 @@
|
|
|
20
52
|
"!dist/**/*.test.*"
|
|
21
53
|
],
|
|
22
54
|
"scripts": {
|
|
23
|
-
"build": "tsc -p tsconfig.build.json",
|
|
55
|
+
"build": "tsc -p tsconfig.build.json && node ../../scripts/schema-to-dist.mjs && node ../../scripts/strip-comments.mjs dist",
|
|
24
56
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
25
57
|
"test": "vitest run --passWithNoTests",
|
|
58
|
+
"coverage": "vitest run --coverage.enabled",
|
|
26
59
|
"lint": "eslint src"
|
|
27
60
|
},
|
|
28
61
|
"repository": {
|
|
@@ -47,6 +80,6 @@
|
|
|
47
80
|
"styleText"
|
|
48
81
|
],
|
|
49
82
|
"devDependencies": {
|
|
50
|
-
"vitest": "^
|
|
83
|
+
"vitest": "^5.0.0"
|
|
51
84
|
}
|
|
52
85
|
}
|