@uniflowed/config 0.0.0-alpha.4 → 0.0.0-alpha.41

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