@uniflowed/config 0.0.0-alpha.9 → 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/index.js CHANGED
@@ -3,10 +3,21 @@
3
3
  // `@uniflowed/config`.
4
4
 
5
5
  export type {
6
+ BuilderSpec,
6
7
  CapabilityJsHost,
7
8
  CoverageThresholds,
9
+ DeployAdapter,
10
+ PackageManagerPreference,
11
+ PackageManagerSpec,
12
+ Permissions,
13
+ PluginEntry,
8
14
  RuleLevel,
15
+ RuntimeEngine,
16
+ RuntimeSpec,
17
+ SizeBudget,
18
+ TaskArgument,
9
19
  TaskDefinition,
20
+ TestRunnerSpec,
10
21
  UniflowedConfig,
11
22
  } from "./internal/schema.js";
12
23
 
@@ -2,9 +2,67 @@
2
2
  //
3
3
  // Owns the Flow shape of `uf.config.js`; `index.js` keeps the public package
4
4
  // entry point thin.
5
+ //
6
+ // # Every key uf reads, and nothing else
7
+ //
8
+ // The guide says a config file "is type-checked as Flow code", which is the
9
+ // whole reason to write one in Flow. That makes an undeclared key worse than a
10
+ // missing feature: `lint.files`, `lint.ignore` and `app.router.enabled` were
11
+ // documented in the configuration reference, accepted by the runtime, and
12
+ // absent from this type — so a project following the reference had to choose
13
+ // between the documented key and a config that checks
14
+ // (ubugeeei-prod/uf#481). They were not the only three.
15
+ //
16
+ // So the key *names* below are no longer a person's job to keep in step with
17
+ // the loader. `crates/uf_config`'s `the_flow_schema_declares_every_key_uf_reads`
18
+ // parses this file with uf's own Flow parser and compares the paths it declares
19
+ // against the paths `uf_config::UniflowedConfig` serializes — in both
20
+ // directions, because a key declared here and read by nothing is an option that
21
+ // silently does nothing, which is the same defect wearing the other face.
22
+ //
23
+ // The value *types* are still this file's own judgement, and deliberately so.
24
+ // A test over names cannot say whether `"biome" | "prettier" | "none"` is the
25
+ // right set, and several keys below are narrower than what the loader will
26
+ // parse — `orm.module` is `"@uniflowed/orm"` because there is one
27
+ // implementation, where the loader takes any string. Where this package means
28
+ // to be more opinionated than the parser, that is what these say.
29
+ //
30
+ // # What an editor shows
31
+ //
32
+ // `uf lsp` completes and explains `uf.config.js` from this file, compiled into
33
+ // the binary. A key's completion shows the comment directly above the key and
34
+ // the type as it is written here, and the members of a literal union are the
35
+ // values it offers — so a comment above a key is written for somebody typing
36
+ // that key. Either kind of comment counts above a key; above a type alias only
37
+ // a `/** */` block does, because the `//` notes on the aliases are this file's
38
+ // history rather than the key's meaning. `crates/uf_config/src/schema.rs` is
39
+ // the reader, and says exactly what it takes.
5
40
 
6
41
  export type RuleLevel = "off" | "warn" | "error" | 0 | 1 | 2 | boolean;
7
42
 
43
+ /**
44
+ * One argument a task takes, filled by `uf run <task>`.
45
+ *
46
+ * Given after the task's name, in the order declared, or as `--name value`; the
47
+ * values are appended to `command` in that order either way. At a terminal, a
48
+ * required argument that was not given is picked from `choices`, or typed; in
49
+ * CI and pipelines it is an error that names it.
50
+ */
51
+ export type TaskArgument = {
52
+ // What `--name` and the picker call it: letters, digits, `-` and `_`.
53
+ readonly name: string,
54
+ // One line saying what it is for, shown in the picker.
55
+ readonly description?: string,
56
+ // The only values it may take. Anything else is refused, and at a terminal
57
+ // these are the list to pick from.
58
+ readonly choices?: $ReadOnlyArray<string>,
59
+ // Used when it is not given. An argument with a default is never asked for.
60
+ readonly default?: string,
61
+ // Whether leaving it out is an error. Defaults to `true` unless there is a
62
+ // `default`; an optional argument with no default must come last.
63
+ readonly required?: boolean,
64
+ };
65
+
8
66
  export type TaskDefinition =
9
67
  | string
10
68
  | {
@@ -12,10 +70,253 @@ export type TaskDefinition =
12
70
  readonly cwd?: string,
13
71
  readonly dependsOn?: $ReadOnlyArray<string>,
14
72
  readonly env?: { readonly [string]: string },
73
+ // Everything the task reads, as paths or globs from the project root; a
74
+ // pattern beginning `!` excludes. This is the whole of the cache key, so
75
+ // a task that lists nothing is never cached and always runs.
76
+ readonly inputs?: $ReadOnlyArray<string>,
77
+ // Everything it writes. Checked rather than restored: a replayed result
78
+ // has to still have its files on disk, unchanged.
79
+ readonly outputs?: $ReadOnlyArray<string>,
80
+ // `false` keeps a task with declared inputs out of the cache.
81
+ readonly cache?: boolean,
82
+ // The arguments `uf run` fills, in the order they are appended to
83
+ // `command`.
84
+ readonly args?: $ReadOnlyArray<TaskArgument>,
15
85
  };
16
86
 
17
87
  export type CapabilityJsHost = "node" | "deno" | "bun";
18
88
 
89
+ /**
90
+ * The package manager uf drives, overriding what it infers from the project.
91
+ *
92
+ * `"auto"` reads the project itself: an explicit `"packageManager"` field, then
93
+ * a lockfile, then the nearest workspace root, then uf's own resolver.
94
+ */
95
+ // The eight below are the *names a project may pin*, not commands this module
96
+ // runs: `@uniflowed/config` declares a type and executes nothing at all.
97
+ export type PackageManagerPreference =
98
+ | "auto"
99
+ | "uf"
100
+ | "npm"
101
+ | "pnpm"
102
+ | "yarn"
103
+ | "yarn-classic"
104
+ | "yarn-berry"
105
+ | "bun";
106
+
107
+ // The runtimes a project may say it is written for, which are the runtimes uf
108
+ // has a host for. It read `"uf" | ... | "edge" | "serverless" | "container"`,
109
+ // and those four are rows `uf_runtime::HOSTS` grades planned with no Flow
110
+ // loader — so a project that named one could not import its own first file
111
+ // there, and every command went on running on Node anyway. uf refuses them at
112
+ // the key that named them now; this type is where a reader finds that out
113
+ // before `uf check` does. See docs/hosts.md and ubugeeei-prod/uf#246.
114
+ //
115
+ // A deployment target is `DeployAdapter`, on `app.runtime.deploy`, and is a
116
+ // different question with a longer list.
117
+ export type RuntimeEngine = "node" | "deno" | "bun";
118
+
119
+ export type DeployAdapter =
120
+ | "node"
121
+ | "bun"
122
+ | "deno"
123
+ | "edge"
124
+ | "serverless"
125
+ | "static"
126
+ | "container";
127
+
128
+ // # Tools, declared where they are used
129
+ //
130
+ // The four aliases below are strings to Flow and a grammar to uf, which reads
131
+ // every one of them where the config is read and refuses what it cannot run —
132
+ // naming the key, what was written, and what to write instead. They are
133
+ // aliases rather than bare `string` so the grammar has one place to be written
134
+ // down, and so an editor can find a tool-spec key by its type — which is what
135
+ // completion in `uf.config.js` keys on (ubugeeei-prod/uf#941). Rename one and
136
+ // that stops working without a type error anywhere.
137
+ //
138
+ // A spec is `name[@version]`, and what follows the `@` is one of three things:
139
+ //
140
+ // * nothing — `"node"` — the `node` on `PATH`, which is what every project
141
+ // got before it could say anything else;
142
+ // * a numeric prefix — `"node@26"`, `"bun@1.4"` — the newest release that
143
+ // starts with it, resolved once against the publisher's index and locked in
144
+ // `uf.lock`, so every machine runs the same release until somebody moves it;
145
+ // * a full version — `"pnpm@12.0.0"` — exactly that release.
146
+ //
147
+ // A range — `"node@^26"`, `"node@>=24"`, `"node@24.x"` — is refused: a range is
148
+ // not an environment, because it can resolve to a different release tomorrow.
149
+ // So is a tag such as `"node@lts"`, for the same reason. ubugeeei-prod/uf#940.
150
+
151
+ /**
152
+ * A JavaScript runtime, and optionally which release of it: `"node"`,
153
+ * `"node@26"`, `"bun@1.3.5"`.
154
+ *
155
+ * The names are `node`, `bun` and `deno`. No version is the one on `PATH`; a
156
+ * prefix is the newest release that starts with it, locked in `uf.lock`; a full
157
+ * version is exactly that release. A range or a tag is refused.
158
+ *
159
+ * The type of `runtime`, `build.runtime` and `test.runtime`.
160
+ */
161
+ export type RuntimeSpec = string;
162
+
163
+ /**
164
+ * A package manager, and optionally which release of it: `"pnpm"`,
165
+ * `"pnpm@10"`, `"pnpm@12.0.0"`.
166
+ *
167
+ * The names are `npm`, `pnpm`, `yarn` and `bun`. Yarn's edition is its major
168
+ * version — `"yarn@1"` is Classic. No version is the one on `PATH`; a prefix is
169
+ * the newest release that starts with it, locked in `uf.lock`; a full version
170
+ * is exactly that release. A range or a tag is refused.
171
+ *
172
+ * The type of `packageManager`.
173
+ */
174
+ export type PackageManagerSpec = string;
175
+
176
+ /**
177
+ * What runs the test suite: `"uf"` or `"bun[@version]"`.
178
+ *
179
+ * `"uf"` is the runner built into uf, and the default; it takes no version,
180
+ * because it is the binary that is running. `"bun"` is `bun test`, on the Bun
181
+ * it names — so it also decides the test runtime when `test.runtime` is
182
+ * absent, and a `test.runtime` naming anything else is an error. The version
183
+ * follows the same grammar as a runtime's. Under a Bun runner `uf test` hands
184
+ * the files its discovery found to `bun test`, with uf's Flow preload, and
185
+ * `@uniflowed/test` resolves to `bun:test`; what Bun has no equivalent for
186
+ * raises `UnsupportedError` by name. See guide/testing, "Choosing a runner".
187
+ *
188
+ * The type of `test.runner`.
189
+ */
190
+ export type TestRunnerSpec = string;
191
+
192
+ /**
193
+ * Which builder `uf dev`, `uf build`, `uf preview` and `uf start` drive:
194
+ * `"vite"`, or a module specifier.
195
+ *
196
+ * `"vite"` is `@uniflowed/vite`, the builder uf ships and the default. Any other
197
+ * string is a module specifier — a package found up `node_modules`, or a path
198
+ * starting with `.` or `/` that must stay inside the project — whose driver
199
+ * satisfies the contract in docs/architecture.md.
200
+ *
201
+ * The type of `build.builder`.
202
+ */
203
+ export type BuilderSpec = string;
204
+
205
+ /**
206
+ * One entry of `plugins: [...]`.
207
+ *
208
+ * A bare name takes the default band and applies to every pipeline; the long
209
+ * form says otherwise. Declaration order decides within a band, so the
210
+ * resolved pipeline is a function of this file alone.
211
+ */
212
+ export type PluginEntry =
213
+ | string
214
+ | {
215
+ readonly name: string,
216
+ readonly order?: "pre" | "normal" | "post",
217
+ readonly apply?: "build" | "serve" | "always",
218
+ };
219
+
220
+ // # `app.router`'s three lists
221
+ //
222
+ // Next.js's `redirects`, `rewrites` and `headers`, written as data rather than
223
+ // as async functions, because this file is read without being run. A `source`
224
+ // is a path in the route table's own grammar — a literal segment, `:name` for
225
+ // one segment, and a trailing `:name*` for the rest — and each rule is an exact
226
+ // object, so a misspelled field is an error here and where `uf` reads the file.
227
+
228
+ /**
229
+ * One redirect: a request for `source` is answered with a redirect to
230
+ * `destination`, before anything else answers — a file included.
231
+ *
232
+ * `destination` is a path of this application or an absolute `http(s)` URL,
233
+ * and may use the `:name` and `:name*` segments `source` declared. The
234
+ * request's query is passed through. `permanent: true` is a `308`, `false` a
235
+ * `307`; both keep the method.
236
+ */
237
+ export type RouteRedirect = {
238
+ readonly source: string,
239
+ readonly destination: string,
240
+ readonly permanent: boolean,
241
+ };
242
+
243
+ /**
244
+ * One rewrite: a request for `source` is answered by the route at
245
+ * `destination`, and the address bar keeps `source`.
246
+ *
247
+ * `destination` is a path of this application — never another origin, which
248
+ * is a route handler that fetches — and may use `source`'s segments. Applied
249
+ * after the build's own files and before middleware, so the middleware that
250
+ * runs is the destination's.
251
+ */
252
+ export type RouteRewrite = {
253
+ readonly source: string,
254
+ readonly destination: string,
255
+ };
256
+
257
+ /**
258
+ * Response headers for every request whose path matches `source`, files
259
+ * included. Every matching rule applies, in order, and a later one setting the
260
+ * same name wins — over the response's own header of that name, too.
261
+ */
262
+ export type RouteHeaders = {
263
+ readonly source: string,
264
+ readonly headers: { readonly [name: string]: string },
265
+ };
266
+
267
+ /**
268
+ * Which spelling of a path is the page.
269
+ *
270
+ * `"never"` answers `/about/` with a `308` to `/about` and writes the page as
271
+ * `about.html`; `"always"` answers `/about` with a `308` to `/about/` and
272
+ * writes `about/index.html`; `"ignore"`, the default, answers both and writes
273
+ * `about/index.html`. `next.config.js`'s `trailingSlash: false`, `true` and
274
+ * `skipTrailingSlashRedirect: true`.
275
+ */
276
+ export type RouterTrailingSlash = "never" | "always" | "ignore";
277
+
278
+ /**
279
+ * A ceiling `uf build` fails over, and what it is measured on.
280
+ *
281
+ * `max` accepts a byte count or a size a person would write — `"180kb"` —
282
+ * because a budget is written by hand and read back by a report.
283
+ */
284
+ export type SizeBudget = {
285
+ readonly max: number | string,
286
+ readonly metric?: "raw" | "gzip" | "brotli",
287
+ };
288
+
289
+ /**
290
+ * What the project's own code may reach.
291
+ *
292
+ * Absent from `uf.config.js` means no permission model: the toolchain starts
293
+ * its host the way it always has. Present means **deny by default** — the
294
+ * project's code gets what is listed and nothing else — and `permissions: {}`
295
+ * is a legitimate thing to write, meaning "nothing beyond what uf itself needs
296
+ * to load and transform the project". There is deliberately no way to spell
297
+ * "everything"; a project that wants everything does not declare a set.
298
+ *
299
+ * Every field is a list of literal strings, because `uf.config.js` is read as
300
+ * text and parsed as JSON5 rather than executed: a computed path or a
301
+ * `process.env` read here would not parse.
302
+ *
303
+ * The set is uf's, not a runtime's. Node.js enforces `read` and `write`, Deno
304
+ * enforces all five, and Bun has no permission model at all — a host that
305
+ * cannot enforce what is declared **refuses the run** rather than applying part
306
+ * of it. `docs/hosts.md` is the table.
307
+ *
308
+ * The declared paths are *added to* the ones uf needs to run the project, so
309
+ * what a set denies is the rest of the machine — `~/.ssh`, the network, the
310
+ * environment — and not the project's own files.
311
+ */
312
+ export type Permissions = {
313
+ readonly read?: $ReadOnlyArray<string>,
314
+ readonly write?: $ReadOnlyArray<string>,
315
+ readonly net?: $ReadOnlyArray<string>,
316
+ readonly env?: $ReadOnlyArray<string>,
317
+ readonly run?: $ReadOnlyArray<string>,
318
+ };
319
+
19
320
  /**
20
321
  * Percentages a coverage gate requires, as whole numbers between 0 and 100.
21
322
  *
@@ -30,7 +331,52 @@ export type CoverageThresholds = {
30
331
  };
31
332
 
32
333
  export type UniflowedConfig = {
334
+ /**
335
+ * The runtime accessibility audit: `expect(el).toHaveNoAxeViolations()` in a
336
+ * test, and the page `uf dev` is serving.
337
+ *
338
+ * One block for both on purpose. A rule a project has decided cannot be
339
+ * judged here — `color-contrast` against a DOM with no layout is the
340
+ * standing example — has to be the same rule in CI and in the loop somebody
341
+ * is working in, or the audit that finds a violation while the component is
342
+ * being written disagrees with the one that blocks the pull request.
343
+ *
344
+ * Inert without axe-core, which uf does not install: add it and both halves
345
+ * start working.
346
+ *
347
+ * Not the same thing as `lint.rules`' `a11y/*`, which read JSX that was
348
+ * never rendered.
349
+ */
350
+ readonly accessibility?: {
351
+ readonly devAudit?: boolean,
352
+ readonly axe?: {
353
+ // Run only rules carrying one of these axe tags; every rule when absent.
354
+ readonly tags?: $ReadOnlyArray<string>,
355
+ readonly disabledRules?: $ReadOnlyArray<string>,
356
+ readonly minImpact?: "minor" | "moderate" | "serious" | "critical",
357
+ },
358
+ },
33
359
  readonly app?: {
360
+ // Whether a component with no directive is rendered on the server or
361
+ // shipped to the browser.
362
+ readonly componentDefault?: "server" | "client",
363
+ readonly framework?: "uniflowed" | "react" | "react-native",
364
+ readonly react?: {
365
+ // React 19's Strict Mode, on by default in `uf dev`: it double-invokes
366
+ // render and effects so an impurity shows up in development rather
367
+ // than in production. Off has to be a choice a project makes.
368
+ readonly strictMode?: boolean,
369
+ readonly version?: string,
370
+ readonly asyncReact?: boolean,
371
+ readonly suspense?: boolean,
372
+ readonly useHook?: boolean,
373
+ },
374
+ // Whether the project builds React Server Components at all, and whether
375
+ // a `"use server"` export is wired to an endpoint.
376
+ readonly rsc?: boolean,
377
+ readonly serverActions?: boolean,
378
+ // The runtimes the build must satisfy.
379
+ readonly targets?: $ReadOnlyArray<"web" | "react-native" | "server" | "hermes">,
34
380
  readonly orm?: {
35
381
  readonly enabled?: boolean,
36
382
  readonly module?: "@uniflowed/orm",
@@ -39,11 +385,18 @@ export type UniflowedConfig = {
39
385
  readonly preparedByDefault?: true,
40
386
  },
41
387
  readonly builtins?: {
388
+ readonly data?: "uniflowed-query",
389
+ readonly effect?: "uniflowed-effect",
42
390
  readonly fetch?: {
43
391
  readonly module?: "@uniflowed/fetch",
44
392
  readonly overrideGlobalFetch?: false,
45
393
  },
46
394
  readonly cell?: boolean,
395
+ readonly frameworkLints?: boolean,
396
+ readonly nativeTestRunner?: boolean,
397
+ readonly reactTestingLibrary?: boolean,
398
+ readonly relay?: boolean,
399
+ readonly style?: "style-x",
47
400
  readonly reactCompiler?: {
48
401
  readonly enabled?: boolean,
49
402
  readonly implementation?: "official-rust",
@@ -66,6 +419,19 @@ export type UniflowedConfig = {
66
419
  readonly extensions?: $ReadOnlyArray<".mdx">,
67
420
  readonly jsxImportSource?: "@uniflowed/jsx-runtime",
68
421
  readonly pipelinePlugin?: "built-in",
422
+ // Colours are computed during the build and written into the HTML,
423
+ // so nothing ships to the browser to do it. Both themes are emitted
424
+ // together as CSS variables, because a build cannot know which the
425
+ // reader prefers.
426
+ readonly highlight?: {
427
+ readonly enabled?: boolean,
428
+ readonly themes?: {
429
+ readonly light?: string,
430
+ readonly dark?: string,
431
+ },
432
+ // Grammars beyond the ones a uf project uses by default.
433
+ readonly langs?: $ReadOnlyArray<string>,
434
+ },
69
435
  },
70
436
  readonly cache?: "opt-in",
71
437
  },
@@ -76,6 +442,34 @@ export type UniflowedConfig = {
76
442
  readonly widths?: $ReadOnlyArray<number>,
77
443
  readonly quality?: number,
78
444
  readonly placeholder?: boolean,
445
+ // The remote images `/__uf/image` may fetch, resize and re-encode —
446
+ // Next.js's `images.remotePatterns`, with its four keys and its
447
+ // wildcards. Empty, the default, means there is no endpoint at all. A
448
+ // redirect is followed only to a URL that matches too. See
449
+ // docs/app/guide/assets.
450
+ readonly remotePatterns?: $ReadOnlyArray<{
451
+ // `"https"` when absent; `"http"` has to be said.
452
+ readonly protocol?: "https" | "http",
453
+ // A name, or one under a leading `*.` (one more label) or `**.`
454
+ // (any number). A bare `*` is refused: it is not an allow-list.
455
+ readonly hostname: string,
456
+ // The protocol's default port when absent, and no other.
457
+ readonly port?: string,
458
+ // `*` is one segment and `**` any number. Any path when absent.
459
+ readonly pathname?: string,
460
+ }>,
461
+ // Qualities the endpoint answers besides `quality`. Each one is one
462
+ // more encode of every remote image a stranger can ask for, so the
463
+ // endpoint takes the listed ones and no others.
464
+ readonly qualities?: $ReadOnlyArray<number>,
465
+ // Fetch from loopback, private and link-local addresses too. For a
466
+ // test that serves its own images; never for a deployment.
467
+ readonly dangerouslyAllowPrivateAddresses?: boolean,
468
+ // A module exporting `createImageTransformer`, for a deploy target
469
+ // with no encoder of its own: `--adapter node`, `bun`, `deno`,
470
+ // `container` and `serverless`. `uf start` and `uf preview` encode
471
+ // with uf, and `--adapter edge` with Cloudflare's image binding.
472
+ readonly transformer?: string,
79
473
  },
80
474
  readonly fonts?: {
81
475
  readonly enabled?: boolean,
@@ -84,6 +478,27 @@ export type UniflowedConfig = {
84
478
  // faces `uf_assets::font::LOCAL_FACES` knows the metrics of, because
85
479
  // the scaling is a ratio against real numbers rather than a guess.
86
480
  readonly fallback?: string,
481
+ // How an imported family is cut down. `"none"` is the default and the
482
+ // argued position: subsetting is lossy, and a build cannot see the
483
+ // text a server will render or a user will type. `"ranges"` splits the
484
+ // font's own coverage into script buckets with exact `unicode-range`
485
+ // values and loses nothing.
486
+ readonly subset?: "none" | "ranges",
487
+ // Whether `Font` preloads the primary face. Exactly one file is ever
488
+ // preloaded, however many buckets a split produced.
489
+ readonly preload?: boolean,
490
+ },
491
+ readonly icons?: {
492
+ readonly enabled?: boolean,
493
+ // Where `uf:icon/<name>` looks for `<name>.svg`, from the project
494
+ // root. A directory rather than a claim on `.svg`, which stays Vite's.
495
+ readonly dir?: string,
496
+ },
497
+ readonly og?: {
498
+ readonly enabled?: boolean,
499
+ // The typeface every `*.og.json` is drawn with unless it names its
500
+ // own. uf embeds none, so a project that draws cards points at one.
501
+ readonly font?: string | null,
87
502
  },
88
503
  readonly motion?: {
89
504
  readonly module?: "@uniflowed/motion",
@@ -118,10 +533,8 @@ export type UniflowedConfig = {
118
533
  },
119
534
  },
120
535
  readonly runtime?: {
121
- readonly default?: "node" | "deno" | "bun" | "uf",
122
- readonly compatibility?: $ReadOnlyArray<
123
- "node" | "bun" | "deno" | "edge" | "serverless" | "container",
124
- >,
536
+ readonly default?: RuntimeEngine,
537
+ readonly compatibility?: $ReadOnlyArray<RuntimeEngine>,
125
538
  readonly capabilityJsHost?: {
126
539
  readonly default?: CapabilityJsHost,
127
540
  readonly hosts?: $ReadOnlyArray<CapabilityJsHost>,
@@ -129,36 +542,199 @@ export type UniflowedConfig = {
129
542
  },
130
543
  readonly deploy?: {
131
544
  readonly enabled?: boolean,
132
- readonly adapters?: $ReadOnlyArray<
133
- "node" | "bun" | "deno" | "edge" | "serverless" | "static" | "container",
134
- >,
545
+ // The target `uf build` writes an artefact for when none is named on
546
+ // the command line; `--adapter` beats it.
547
+ readonly adapter?: DeployAdapter,
548
+ readonly adapters?: $ReadOnlyArray<DeployAdapter>,
135
549
  },
136
550
  },
137
551
  readonly router?: {
552
+ // `false` says this project is not a uf application, and is the only way
553
+ // to say it: a library has no routes to scan for.
554
+ readonly enabled?: boolean,
138
555
  readonly entry?: string,
139
556
  readonly root?: string,
140
557
  readonly manifest?: string,
558
+ readonly convention?: "file-system",
559
+ // Turning the file-system router off is what makes a project a
560
+ // **library** rather than an application, and `uf build` reads it: see
561
+ // `build.lib` below and docs/app/reference/config.
562
+ readonly enabled?: boolean,
563
+ /**
564
+ * Redirects answered before anything else, in order; the first whose
565
+ * `source` matches wins. `next.config.js`'s `redirects()`. See
566
+ * `RouteRedirect` and docs/app/guide/routing.
567
+ */
568
+ readonly redirects?: $ReadOnlyArray<RouteRedirect>,
569
+ /**
570
+ * Routes served at another path, in order; the first whose `source`
571
+ * matches wins. `next.config.js`'s `rewrites()`, without proxying. See
572
+ * `RouteRewrite` and docs/app/guide/routing.
573
+ */
574
+ readonly rewrites?: $ReadOnlyArray<RouteRewrite>,
575
+ /**
576
+ * Response headers by path; every matching rule applies.
577
+ * `next.config.js`'s `headers()`. See `RouteHeaders` and
578
+ * docs/app/guide/routing.
579
+ */
580
+ readonly headers?: $ReadOnlyArray<RouteHeaders>,
581
+ /**
582
+ * The path the whole application is served under — `"/docs"` — with no
583
+ * trailing slash. Every link, redirect, asset URL, payload URL and
584
+ * sitemap entry carries it, and a request outside it is a `404`. The
585
+ * sources of the three lists above are written without it.
586
+ * `next.config.js`'s `basePath`.
587
+ */
588
+ readonly basePath?: string,
589
+ /**
590
+ * Which spelling of a path is the page; see `RouterTrailingSlash`.
591
+ * `"ignore"` when absent.
592
+ */
593
+ readonly trailingSlash?: RouterTrailingSlash,
594
+ /** HTTPS links explicitly claimed by the iOS and Android applications. */
595
+ readonly nativeLinks?: {|
596
+ readonly origins: $ReadOnlyArray<string>,
597
+ readonly routes: $ReadOnlyArray<string>,
598
+ readonly iosAppIds: $ReadOnlyArray<string>,
599
+ readonly androidPackage: string,
600
+ readonly androidSha256: $ReadOnlyArray<string>,
601
+ |},
141
602
  },
142
603
  readonly rendering?: {
143
- readonly modes?: $ReadOnlyArray<"ppr" | "ssr" | "ssg" | "isr">,
604
+ // `"csr"` is the one value that cannot share the list: it renders every
605
+ // route in the browser from one shell, where the others write a document
606
+ // per route, so there is no per-route choice left for the list to hold.
607
+ // `["csr"]` is a single-page application; see docs/app/guide/rendering.
608
+ readonly modes?: $ReadOnlyArray<"ppr" | "ssr" | "ssg" | "isr" | "csr">,
609
+ // What the browser does when a visitor follows a link, which is a
610
+ // different question from `modes` rather than a fifth value in it:
611
+ // `modes` says where a document comes from, per route, and this says
612
+ // what happens once the browser has one. `"document"` is a full document
613
+ // request — the client router is not installed and `Link` renders an
614
+ // ordinary anchor. See docs/app/guide/routing/navigation.
615
+ readonly navigation?: "client" | "document",
616
+ // How long, in whole seconds, the client router shows a route it already
617
+ // fetched or prefetched without asking the server again: a click on a
618
+ // prefetched link, a second visit and the back button all read it. `0`,
619
+ // the default, keeps nothing. `router.refresh()` and every server action
620
+ // clear it. See docs/app/guide/routing and docs/app/guide/cache.
621
+ readonly staleTime?: number,
144
622
  readonly cache?: {
145
623
  readonly actions?: boolean,
146
624
  readonly data?: boolean,
147
625
  readonly fetch?: boolean,
148
626
  readonly route?: boolean,
627
+ // Where cache entries live, and the only thing here that is a *name*
628
+ // rather than a switch: `"memory"` (the default) is one process,
629
+ // `"filesystem"` is uf's built-in durable provider, and anything else
630
+ // is a module specifier exporting `createCacheProvider` — the same
631
+ // shape `builder.module` has, and for the same red line. Turning this
632
+ // on caches nothing new: a route still has to state a lifetime.
633
+ readonly store?: string,
634
+ // Where `"filesystem"` keeps them. Defaults to `.uf/cache/route` under
635
+ // the project. A deployment that has one writable directory — `/tmp` on
636
+ // a Lambda, a mounted volume in a container — names it here.
637
+ readonly storeDir?: string,
149
638
  },
150
639
  },
151
640
  },
152
641
  readonly build?: {
642
+ // Size ceilings that fail the build. Unset by default: failing a build
643
+ // nobody asked uf to police is worse than reporting the size.
644
+ readonly budgets?: {
645
+ readonly total?: SizeBudget,
646
+ readonly initialJs?: SizeBudget,
647
+ readonly perRoute?: SizeBudget,
648
+ readonly perAsset?: SizeBudget,
649
+ },
153
650
  readonly entries?: $ReadOnlyArray<string>,
651
+ // Commands to run around the build, in the same shape as `tasks`.
652
+ readonly hooks?: { readonly [string]: TaskDefinition },
154
653
  readonly outDir?: string,
654
+ // Prerender every route and leave no server bundle behind. Read together
655
+ // with `app.rendering.modes`; see docs/app/reference/config.
155
656
  readonly staticBuild?: boolean,
156
657
  readonly sourcemap?: boolean,
658
+ /**
659
+ * What a **library** build writes, for a project whose
660
+ * `app.router.enabled` is false.
661
+ *
662
+ * Never the switch. `app.router.enabled` decides which of the two builds
663
+ * `uf build` runs, and declaring this key in a project whose router is on
664
+ * is refused where the config is read rather than resolved by precedence.
665
+ * Every field has a default that builds what `uf new --lib` scaffolds, so
666
+ * a library ordinarily writes none of this.
667
+ *
668
+ * `"umd"` and `"iife"` are deliberately absent from `formats`: each needs
669
+ * a global name per entry, and what a Flow library's global should be is
670
+ * not a decision uf has made.
671
+ */
672
+ readonly lib?: {
673
+ // The modules to build, relative to the project root. Each names its own
674
+ // output by its path — `internal/parse.js` is written to
675
+ // `dist/internal/parse.js` — so two entries cannot collide.
676
+ readonly entries?: $ReadOnlyArray<string>,
677
+ readonly formats?: $ReadOnlyArray<"es" | "cjs">,
678
+ // Package names to leave as imports *beyond* the ones the manifest
679
+ // declares. A library build already externalises `dependencies`,
680
+ // `peerDependencies`, `optionalDependencies` and the host's built-in
681
+ // modules; this is for what a manifest cannot say.
682
+ readonly external?: $ReadOnlyArray<string>,
683
+ /**
684
+ * Write a TypeScript declaration file beside each entry. On by
685
+ * default.
686
+ *
687
+ * Most people who install a Flow library write TypeScript, and to them
688
+ * a package with no `.d.ts` is `any`. Every Flow construct that has no
689
+ * TypeScript meaning is named in the build report rather than silently
690
+ * widened, so turning this off is for a library that would rather ship
691
+ * no declarations than ones with gaps in them — not for one that wants
692
+ * to stop hearing about the gaps.
693
+ */
694
+ readonly declarations?: boolean,
695
+ },
696
+ /**
697
+ * What `uf dev`, `uf build` and `uf preview` run on, when it is not the
698
+ * top-level `runtime`: `"node@26"`.
699
+ *
700
+ * Read before `runtime`, so a project that builds on Node and tests on Bun
701
+ * can say so. See `RuntimeSpec` for the grammar.
702
+ */
703
+ readonly runtime?: RuntimeSpec,
704
+ /**
705
+ * Which builder `uf dev`, `uf build`, `uf preview` and `uf start` drive:
706
+ * `"vite"`, or a module specifier.
707
+ *
708
+ * Vite is the default, not a dependency: any module satisfying the
709
+ * contract in docs/architecture.md can be named here. See `BuilderSpec`.
710
+ */
711
+ readonly builder?: BuilderSpec,
712
+ },
713
+ readonly builder?: {
714
+ /**
715
+ * The old spelling of `build.builder`.
716
+ *
717
+ * **Deprecated**, and read only when `build.builder` is absent; the two
718
+ * naming different builders is an error. `"@uniflowed/vite"` here is
719
+ * `builder: "vite"` there, and any other specifier moves as it is.
720
+ */
721
+ readonly module?: string,
157
722
  },
158
723
  readonly dev?: {
159
724
  readonly host?: string,
160
725
  readonly port?: number,
161
726
  readonly strictPort?: boolean,
727
+ // Which files the dev server may serve. `deny` is evaluated on the
728
+ // canonical path and beats `allow`; see docs/security.md.
729
+ readonly fs?: {
730
+ readonly allow?: $ReadOnlyArray<string>,
731
+ readonly deny?: $ReadOnlyArray<string>,
732
+ },
733
+ // `--host` refuses to bind a routable address while this is empty: a dev
734
+ // server reachable from the network with no host allow-list is a file
735
+ // server for your source tree.
736
+ readonly allowedHosts?: $ReadOnlyArray<string>,
737
+ readonly allowedOrigins?: $ReadOnlyArray<string>,
162
738
  },
163
739
  readonly docs?: {
164
740
  readonly enabled?: boolean,
@@ -168,13 +744,27 @@ export type UniflowedConfig = {
168
744
  readonly staticBuild?: boolean,
169
745
  readonly deploy?: "void",
170
746
  },
171
- readonly lint?: {
172
- readonly engine?: "rust",
173
- readonly flow?: {
174
- readonly builtins?: "mixed",
175
- readonly parser?: "official-flow-rust",
176
- },
177
- readonly rules?: { readonly [string]: RuleLevel },
747
+ /**
748
+ * The `.env` cascade and the mode it is read for.
749
+ *
750
+ * `active` empty means the command decides — `development` for `uf dev`,
751
+ * `production` for a build, `test` for `uf test`. `files` empty selects the
752
+ * conventional cascade rather than no files at all.
753
+ */
754
+ readonly env?: {
755
+ readonly active?: string,
756
+ readonly files?: $ReadOnlyArray<string>,
757
+ /**
758
+ * Runtimes and package managers by exact version — `{ node: "24.14.0" }`.
759
+ *
760
+ * **Deprecated.** It says which tools a project has and not what each is
761
+ * for, so a project that builds on Node and tests on Bun could not write
762
+ * that down. Declare each tool where it is used instead — `runtime`,
763
+ * `build.runtime`, `test.runtime` and `packageManager` — as
764
+ * `name@version`. It keeps working for `uf env install` and `uf env exec`,
765
+ * and a pin here that disagrees with one of those keys is an error.
766
+ */
767
+ readonly toolchain?: { readonly [string]: string },
178
768
  },
179
769
  readonly fmt?: {
180
770
  readonly indentWidth?: number,
@@ -186,10 +776,57 @@ export type UniflowedConfig = {
186
776
  },
187
777
  readonly nonFlow?: {
188
778
  readonly formatter?: "biome" | "prettier" | "none",
779
+ /**
780
+ * Extra arguments, passed to that formatter verbatim.
781
+ *
782
+ * Strings rather than a shape, so that reaching one of biome's or
783
+ * prettier's own options never waits for a uf release — a Tailwind 4
784
+ * project writes `["--css-parse-tailwind-directives=true"]` and needs no
785
+ * second configuration file. An argument that would turn `uf fmt
786
+ * --check` into a write is refused where the config is read.
787
+ */
788
+ readonly arguments?: $ReadOnlyArray<string>,
189
789
  },
190
790
  readonly quotes?: "single" | "double",
191
791
  readonly semicolons?: boolean,
192
792
  },
793
+ /**
794
+ * Paths no command walks into.
795
+ *
796
+ * Top level because every command that walks the project reads it: `uf fmt`,
797
+ * `uf lint`, `uf check`, `uf test` and `uf doc`. A bare name — `dist` — names
798
+ * a kind of directory and matches at any depth; a path — `src/generated` —
799
+ * names one place. `.uf` and `.git` are uf's and git's and are not a
800
+ * project's to opt back into.
801
+ *
802
+ * Absent takes uf's own list, `["node_modules", "dist", "target"]`. An empty
803
+ * list is a different instruction: it is a project that has looked at that
804
+ * list and wants none of it.
805
+ */
806
+ readonly ignore?: $ReadOnlyArray<string>,
807
+ readonly lint?: {
808
+ readonly engine?: "rust",
809
+ // Globs to lint.
810
+ readonly files?: $ReadOnlyArray<string>,
811
+ /**
812
+ * The old spelling of the top-level `ignore`.
813
+ *
814
+ * **Deprecated**, and read only when `ignore` is absent. It was never
815
+ * `uf lint`'s alone: `uf fmt`, `uf check`, `uf test` and `uf doc` walk the
816
+ * project through the same code and have always obeyed it, so the key was
817
+ * named after one of the five commands that read it. It keeps working for
818
+ * as long as alpha lasts, and every one of those commands says so once.
819
+ * See ubugeeei-prod/uf#575.
820
+ */
821
+ readonly ignore?: $ReadOnlyArray<string>,
822
+ readonly flow?: {
823
+ readonly builtins?: "mixed",
824
+ readonly parser?: "official-flow-rust",
825
+ },
826
+ // Changes to uf's rule table, not the whole of it: a rule you did not
827
+ // mention keeps the level uf ships. Say `"off"` to switch one off.
828
+ readonly rules?: { readonly [string]: RuleLevel },
829
+ },
193
830
  readonly package?: {
194
831
  readonly generator?: "napi-rs",
195
832
  readonly targets?: $ReadOnlyArray<
@@ -197,12 +834,73 @@ export type UniflowedConfig = {
197
834
  >,
198
835
  readonly typescriptDeclarationsToFlow?: true,
199
836
  },
837
+ /**
838
+ * The package manager `uf install`, `uf add`, `uf update` and the rest drive,
839
+ * and optionally which release of it: `"pnpm@12.0.0"`.
840
+ *
841
+ * Read before `pm.packageManager` — its deprecated spelling — and before
842
+ * `package.json#packageManager` and the lockfile. See `PackageManagerSpec`.
843
+ */
844
+ readonly packageManager?: PackageManagerSpec,
845
+ readonly permissions?: Permissions,
846
+ // Plugins the project adds, appended to uf's own and resolved in the order
847
+ // they are written. A name that names a file is code to run, so `uf_plugin`
848
+ // refuses any that reaches outside the project root.
849
+ readonly plugins?: $ReadOnlyArray<PluginEntry>,
200
850
  readonly pm?: {
201
851
  readonly module?: "@uniflowed/pm",
202
852
  readonly resolver?: "uf-native",
203
853
  readonly lockfile?: "uf.lock",
204
854
  readonly storeDir?: string,
205
855
  readonly allowLifecycleScripts?: false,
856
+ /**
857
+ * The package manager uf drives, in the spelling that came before the
858
+ * top-level `packageManager`.
859
+ *
860
+ * **Deprecated** for the managers `packageManager` can name — write
861
+ * `packageManager: "pnpm"` rather than `pm: { packageManager: "pnpm" }`,
862
+ * and `"yarn@1"` for `"yarn-classic"` — and read only when that key is
863
+ * absent. The two naming different managers is an error. `"uf"`, uf's own
864
+ * resolver, has no other spelling and is not deprecated.
865
+ */
866
+ readonly packageManager?: PackageManagerPreference,
867
+ /**
868
+ * The registry uf *reads* from: packuments, provenance attestations, and
869
+ * the versions `uf update` reports against.
870
+ *
871
+ * Unset means `publish.registry`, which is where this lived until it
872
+ * turned out to be answering two questions with one value. A project that
873
+ * publishes to a company registry and installs through a read-through
874
+ * mirror sets both; one that has only ever set `publish.registry` keeps
875
+ * working and is told, once, which key to move to.
876
+ */
877
+ readonly registry?: string,
878
+ /**
879
+ * Which registry answers for which scope: `{ "@company": "https://…" }`.
880
+ *
881
+ * A scope named here resolves from that registry **and nowhere else**.
882
+ * There is no fallback to the public registry, deliberately: publishing
883
+ * `@company/internal-thing` to npmjs and waiting for a resolver to fall
884
+ * back to it is the dependency-confusion attack, so the fallback is the
885
+ * vulnerability rather than a recovery from it. A name the bound registry
886
+ * does not have is an error that names the scope and the registry.
887
+ *
888
+ * uf refuses an install whose lockfile resolves a bound scope from
889
+ * somewhere else. It does not rewrite the project's `.npmrc`: the manager
890
+ * that resolves is the manager that has to be told, in its own
891
+ * configuration.
892
+ */
893
+ readonly scopes?: { readonly [scope: string]: string },
894
+ /**
895
+ * How hard `uf install` looks at npm provenance attestations.
896
+ *
897
+ * `"report"`, the default, reads the attestation of every package the
898
+ * install brought in or moved: an attestation that is not about the
899
+ * tarball being installed stops the install, and a package with none is a
900
+ * line in the summary. `"off"` reads none, for a machine with no route to
901
+ * a registry.
902
+ */
903
+ readonly provenance?: "report" | "off",
206
904
  },
207
905
  readonly rm?: {
208
906
  readonly module?: "@uniflowed/rm",
@@ -213,6 +911,17 @@ export type UniflowedConfig = {
213
911
  readonly apply?: "config-and-host",
214
912
  readonly doctor?: boolean,
215
913
  },
914
+ /**
915
+ * The runtime every command runs on unless a section names its own, and
916
+ * optionally which release of it: `"node@26"`.
917
+ *
918
+ * `uf start`, `uf run` and `uf exec` read it directly. `uf dev`, `uf build`
919
+ * and `uf preview` read `build.runtime` first; `uf test` reads `test.runtime`
920
+ * and the runtime its runner brings first. Absent, a command starts
921
+ * `app.runtime.capabilityJsHost` from `PATH`, as it always has. See
922
+ * `RuntimeSpec`.
923
+ */
924
+ readonly runtime?: RuntimeSpec,
216
925
  readonly server?: {
217
926
  readonly engine?: "native-rust",
218
927
  readonly native?: {
@@ -223,6 +932,19 @@ export type UniflowedConfig = {
223
932
  >,
224
933
  },
225
934
  },
935
+ // Where the built site is served from, and what `uf build` may therefore
936
+ // write for a crawler. `url` is the switch: without it no `sitemap.xml` and
937
+ // no `robots.txt` are written at all, because a build cannot guess the host
938
+ // it will be deployed to and a wrong `<loc>` is worse than a missing one.
939
+ readonly site?: {
940
+ readonly url?: string,
941
+ readonly sitemap?: boolean,
942
+ readonly robots?: {
943
+ readonly enabled?: boolean,
944
+ readonly allow?: $ReadOnlyArray<string>,
945
+ readonly disallow?: $ReadOnlyArray<string>,
946
+ },
947
+ },
226
948
  readonly std?: {
227
949
  readonly module?: "@uniflowed/std",
228
950
  readonly wintertcAligned?: true,
@@ -311,13 +1033,42 @@ export type UniflowedConfig = {
311
1033
  },
312
1034
  readonly test?: {
313
1035
  readonly module?: "@uniflowed/test",
314
- readonly runner?: {
315
- readonly runtime?: "capability-js-host" | "uf-self-hosted",
316
- readonly jsHosts?: $ReadOnlyArray<CapabilityJsHost>,
317
- readonly scheduler?: "native-work-stealing",
318
- readonly performanceTarget?: "faster-than-bun",
319
- readonly officialFlowParser?: true,
320
- },
1036
+ /**
1037
+ * What `uf test` runs on, when it is neither the runtime the runner brings
1038
+ * nor the top-level `runtime`: `"node@26"`.
1039
+ *
1040
+ * A runner that brings its own — `runner: "bun@1.4"` — decides this when it
1041
+ * is absent, and a `runtime` here naming anything else is an error. See
1042
+ * `RuntimeSpec`.
1043
+ */
1044
+ readonly runtime?: RuntimeSpec,
1045
+ /**
1046
+ * Which application host `uf test` targets.
1047
+ *
1048
+ * `"auto"` follows `app.framework`: React Native projects target
1049
+ * `"react-native"`, and every other project targets `"web"`. Write
1050
+ * `"web"` in a React Native project only for tests that intentionally
1051
+ * target a document.
1052
+ */
1053
+ readonly target?: "auto" | "web" | "react-native",
1054
+ /**
1055
+ * What runs the suite: `"uf"`, the default, or `"bun[@version]"`. See
1056
+ * `TestRunnerSpec`.
1057
+ *
1058
+ * The object is the old description of uf's own runner, field by field,
1059
+ * and is **deprecated**: it still parses, and `applicationTarget` in it is
1060
+ * still read when `target` is absent.
1061
+ */
1062
+ readonly runner?:
1063
+ | TestRunnerSpec
1064
+ | {
1065
+ readonly applicationTarget?: "auto" | "web" | "react-native",
1066
+ readonly runtime?: "vite-task" | "capability-js-host" | "uf-self-hosted",
1067
+ readonly jsHosts?: $ReadOnlyArray<CapabilityJsHost>,
1068
+ readonly scheduler?: "vite-task-cache" | "native-work-stealing",
1069
+ readonly performanceTarget?: "vite-task" | "faster-than-bun",
1070
+ readonly officialFlowParser?: true,
1071
+ },
321
1072
  readonly reactTestingLibraryNative?: true,
322
1073
  /**
323
1074
  * What `uf test --coverage` measures, writes and fails on.
@@ -338,6 +1089,38 @@ export type UniflowedConfig = {
338
1089
  },
339
1090
  },
340
1091
  readonly tasks?: { readonly [string]: TaskDefinition },
1092
+ /**
1093
+ * Tasks `uf prepare` runs before a commit, keyed by a glob over the staged
1094
+ * files.
1095
+ *
1096
+ * A glob with no `/` matches a file's name wherever it is; one with a `/`
1097
+ * matches its path from the project root. Each task named runs once, with
1098
+ * every staged file its glob matches appended to its command, over what is
1099
+ * staged rather than what is on disk. A fix to a fully staged file is staged
1100
+ * with it; a fix to a half-staged one is not kept, and stops the commit.
1101
+ */
1102
+ readonly staged?: { readonly [string]: string | $ReadOnlyArray<string> },
1103
+ /**
1104
+ * Where `uf ui add` writes the components a project owns, and where
1105
+ * `uf ui list` and `uf ui diff` look for them.
1106
+ *
1107
+ * `directory` is relative to the project root, and `app/components/ui` when
1108
+ * absent. Every component goes in the one directory, because each imports
1109
+ * the ones it builds on as siblings (`./button.js`). A path that leaves the
1110
+ * project is refused before anything is written.
1111
+ */
1112
+ readonly ui?: {
1113
+ readonly directory?: string,
1114
+ },
1115
+ /**
1116
+ * Vite's own configuration, merged over the one uf generates.
1117
+ *
1118
+ * uf reads none of it, which is the point: an option Vite adds tomorrow
1119
+ * works in a uf project tomorrow rather than after a uf release that names
1120
+ * it. Deliberately unshaped for the same reason — a Flow type over Vite's
1121
+ * options would be the re-declaration this key exists to avoid.
1122
+ */
1123
+ readonly vite?: { readonly [string]: mixed },
341
1124
  readonly vrt?: {
342
1125
  readonly enabled?: boolean,
343
1126
  readonly module?: "@uniflowed/vrt",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/config",
3
- "version": "0.0.0-alpha.9",
3
+ "version": "0.2.0",
4
4
  "description": "Flow declarations for @uniflowed/config, part of the Unified Toolchain for Flow.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "files": [
17
17
  "index.js",
18
- "internal"
18
+ "internal",
19
+ "!*.test.js"
19
20
  ]
20
21
  }