@zerotal/core 1.7.3 → 1.7.5
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/CHANGELOG.md +4 -0
- package/api-surface.md +10 -67
- package/package.json +1 -1
- package/src/application/Application.ts +18 -2
- package/src/assets/assets.ts +56 -4
- package/src/assets/index.ts +9 -4
- package/src/build/PackageLinter.ts +56 -0
- package/src/events/FrameworkEvents.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [1.7.3] — 2026-08-20
|
|
12
|
+
|
|
11
13
|
### Fixed
|
|
12
14
|
|
|
13
15
|
- **`make:request` generates a file that compiles.** The stub imported `@zerotal/validator`,
|
|
@@ -37,6 +39,8 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
37
39
|
actually installs. Each generator's own test had only checked that the output mentioned the
|
|
38
40
|
class being made, which is why none of this showed.
|
|
39
41
|
|
|
42
|
+
## [1.7.2] — 2026-08-18
|
|
43
|
+
|
|
40
44
|
### Added
|
|
41
45
|
|
|
42
46
|
- **`@zerotal/core/errors`** — a subpath for the error classes, so a module that can run in a
|
package/api-surface.md
CHANGED
|
@@ -686,8 +686,6 @@ function data_get = (target: unknown, key: string, defaultValue?: unknown) => un
|
|
|
686
686
|
|
|
687
687
|
function deepMerge = <T extends object>(base: T, override: DeepPartial<T>) => T
|
|
688
688
|
|
|
689
|
-
function deployEnv = () => string
|
|
690
|
-
|
|
691
689
|
function devSurfacesEnabled = () => boolean
|
|
692
690
|
|
|
693
691
|
function env = { (key: string): string | undefined; (key: string, fallback: string): string; (key: string, fallback: boolean): boolean; (key: string, fallback: number): number;}
|
|
@@ -704,8 +702,6 @@ function inject = (...tokens: BindingToken[]) => (target: new (...args: any[]) =
|
|
|
704
702
|
|
|
705
703
|
function isDevSurfaceAllowed = (env: string) => boolean
|
|
706
704
|
|
|
707
|
-
function isProdLike = (env: string) => boolean
|
|
708
|
-
|
|
709
705
|
function json = (data: unknown, status?: number) => void
|
|
710
706
|
|
|
711
707
|
function make = <T>(token: BindingToken<T>) => Promise<T>
|
|
@@ -728,8 +724,6 @@ function redirect = { (url: string, status?: 301 | 302 | 303 | 307 | 308): Re
|
|
|
728
724
|
|
|
729
725
|
function redirectTo = <N extends RouteTarget>(name: N, params?: RouteParamsArg<N>, status?: 301 | 302 | 303 | 307 | 308) => ResponseBuilder
|
|
730
726
|
|
|
731
|
-
function registerAppScope = (installer: AppScopeInstaller) => void
|
|
732
|
-
|
|
733
727
|
function registerErrorDiagnoser = (diagnoser: ErrorDiagnoser) => void
|
|
734
728
|
|
|
735
729
|
function registerFileRouteResolver = (resolver: FileRouteResolver) => void
|
|
@@ -754,8 +748,6 @@ function setAppEnv = (command?: string) => void
|
|
|
754
748
|
|
|
755
749
|
function setCurrentPageResolver = (resolver: CurrentPageResolver) => void
|
|
756
750
|
|
|
757
|
-
function setImplicitModelResolver = (fn: ((paramName: string) => ModelBindingResolver | undefined) | null) => void
|
|
758
|
-
|
|
759
751
|
function sha256Hex = (input: string | Uint8Array) => string
|
|
760
752
|
|
|
761
753
|
function singularize = (value: string) => string
|
|
@@ -1112,9 +1104,7 @@ type AppScopeInstaller = () => () => void
|
|
|
1112
1104
|
|
|
1113
1105
|
type ArgDef = { name: string; required?: boolean; default?: string;}
|
|
1114
1106
|
|
|
1115
|
-
type BindingToken =
|
|
1116
|
-
|
|
1117
|
-
type ClassRef = abstract new (...args: never[]) => unknown
|
|
1107
|
+
type BindingToken = ClassToken<T> | AbstractToken<T> | keyof ContainerBindings
|
|
1118
1108
|
|
|
1119
1109
|
type ContextKey = never
|
|
1120
1110
|
|
|
@@ -1140,7 +1130,7 @@ type FileRouteResolver = (ctx: FileRouteContext) => boolean
|
|
|
1140
1130
|
|
|
1141
1131
|
type FileRoutingConfig = { [x: string]: FileRoutingEntry;}
|
|
1142
1132
|
|
|
1143
|
-
type FileRoutingEntry = string | {
|
|
1133
|
+
type FileRoutingEntry = string | { dir: string; prefix?: string; middleware?: MiddlewareInput }
|
|
1144
1134
|
|
|
1145
1135
|
type FlagDef = { name: string; short?: string; type: 'string' | 'boolean' | 'number'; description?: string; default?: unknown;}
|
|
1146
1136
|
|
|
@@ -1178,7 +1168,7 @@ type RouteTarget = string
|
|
|
1178
1168
|
|
|
1179
1169
|
type RoutingConfig = { [x: string]: RoutingEntry;}
|
|
1180
1170
|
|
|
1181
|
-
type RoutingEntry = string | {
|
|
1171
|
+
type RoutingEntry = string | { file: string; prefix?: string; middleware?: MiddlewareInput }
|
|
1182
1172
|
|
|
1183
1173
|
type SameSite = 'Strict' | 'Lax' | 'None'
|
|
1184
1174
|
|
|
@@ -1526,35 +1516,6 @@ class CssBuildCommand = {
|
|
|
1526
1516
|
write: (msg: string) => void
|
|
1527
1517
|
}
|
|
1528
1518
|
|
|
1529
|
-
class DeployCommand = {
|
|
1530
|
-
new (): DeployCommand
|
|
1531
|
-
static args: ArgDef[]
|
|
1532
|
-
static commandName: string
|
|
1533
|
-
static description: string
|
|
1534
|
-
static flags: ({ name: string; type: 'boolean'; description: string; default: boolean;} | { name: string; type: 'string'; description: string; default?: never;})[]
|
|
1535
|
-
static needsApp: boolean
|
|
1536
|
-
static target: string
|
|
1537
|
-
_readLine: () => Promise<string>
|
|
1538
|
-
_writer: OutputWriter
|
|
1539
|
-
app: unknown
|
|
1540
|
-
args: Record<string, string>
|
|
1541
|
-
ask: (question: string, defaultValue?: string) => Promise<string>
|
|
1542
|
-
choice: (question: string, options: string[]) => Promise<string>
|
|
1543
|
-
confirm: (question: string, defaultValue?: boolean) => Promise<boolean>
|
|
1544
|
-
dim: (msg: string) => void
|
|
1545
|
-
error: (msg: string) => void
|
|
1546
|
-
flags: Record<string, string | number | boolean>
|
|
1547
|
-
info: (msg: string) => void
|
|
1548
|
-
line: (msg: string) => void
|
|
1549
|
-
newLine: () => void
|
|
1550
|
-
run: () => Promise<void>
|
|
1551
|
-
secret: (question: string) => Promise<string>
|
|
1552
|
-
section: (title: string) => void
|
|
1553
|
-
table: (rows: [string, string][], indent?: number) => void
|
|
1554
|
-
warn: (msg: string) => void
|
|
1555
|
-
write: (msg: string) => void
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
1519
|
class DevCommand = {
|
|
1559
1520
|
new (): DevCommand
|
|
1560
1521
|
static aliases: string[]
|
|
@@ -2316,8 +2277,6 @@ class WorkerCommand = {
|
|
|
2316
2277
|
write: (msg: string) => void
|
|
2317
2278
|
}
|
|
2318
2279
|
|
|
2319
|
-
function makeDeployCommand = (target: string) => DeployCommandClass
|
|
2320
|
-
|
|
2321
2280
|
## ./config `(./src/config/index.ts)`
|
|
2322
2281
|
|
|
2323
2282
|
class ConfigLoader = {
|
|
@@ -2429,7 +2388,7 @@ interface RegisteredConfigValidator = {
|
|
|
2429
2388
|
validate: ConfigValidator
|
|
2430
2389
|
}
|
|
2431
2390
|
|
|
2432
|
-
type AssetLoaderKind = '
|
|
2391
|
+
type AssetLoaderKind = 'file' | 'dataurl' | 'base64' | 'text' | 'json' | 'toml'
|
|
2433
2392
|
|
|
2434
2393
|
type ConfigIssueLevel = 'error' | 'warning'
|
|
2435
2394
|
|
|
@@ -2511,8 +2470,6 @@ const SERVER_PROCESS_NAME = 'server'
|
|
|
2511
2470
|
|
|
2512
2471
|
function bootBuildDecision = (outDirs: string[], env: string | undefined) => Promise<BootBuildDecision>
|
|
2513
2472
|
|
|
2514
|
-
function browserEnvDefines = (isProduction: boolean) => Record<string, string>
|
|
2515
|
-
|
|
2516
2473
|
function buildCssBundle = (input: string, outdir: string, minify?: boolean, loader?: Record<string, string>) => Promise<BundleResult>
|
|
2517
2474
|
|
|
2518
2475
|
function buildJsBundle = (input: string, outdir: string, minify?: boolean) => Promise<BundleResult>
|
|
@@ -2523,14 +2480,8 @@ function createDeck = (options: DeckOptions) => Deck
|
|
|
2523
2480
|
|
|
2524
2481
|
function detectCssPlugins = (cwd: string) => Promise<BunPlugin[]>
|
|
2525
2482
|
|
|
2526
|
-
function isDevOrchestrated = (env?: Record<string, string | undefined>, argv?: readonly string[]) => boolean
|
|
2527
|
-
|
|
2528
2483
|
function isWritableDir = (dir: string) => Promise<boolean>
|
|
2529
2484
|
|
|
2530
|
-
function pruneBuildOutput = (outdir: string, outputs: readonly { path: string;}[]) => Promise<string[]>
|
|
2531
|
-
|
|
2532
|
-
function registerDevBuildHook = (name: string, fn: BuildHookFn) => void
|
|
2533
|
-
|
|
2534
2485
|
function registerDevHtmlSnippet = (name: string, fn: DevHtmlSnippet) => void
|
|
2535
2486
|
|
|
2536
2487
|
function startDevMode = (options: StartDevModeOptions) => Promise<void>
|
|
@@ -2646,7 +2597,7 @@ const t = { readonly string: () => Def<string | undefined>; readonly numbe
|
|
|
2646
2597
|
|
|
2647
2598
|
type EnvOutput = { readonly [K in keyof S]: InferDef<S[K]>; }
|
|
2648
2599
|
|
|
2649
|
-
type FieldType = 'string' | 'number' | 'boolean' | '
|
|
2600
|
+
type FieldType = 'string' | 'number' | 'boolean' | 'enum' | 'url' | 'port'
|
|
2650
2601
|
|
|
2651
2602
|
type InferDef = D extends Def<infer T> ? T : never
|
|
2652
2603
|
|
|
@@ -3197,7 +3148,7 @@ interface WebContext = {
|
|
|
3197
3148
|
view: (markup: string | { toString(): string;}, status?: number) => void
|
|
3198
3149
|
}
|
|
3199
3150
|
|
|
3200
|
-
type AnsiColor = '
|
|
3151
|
+
type AnsiColor = 'red' | 'green' | 'yellow' | 'blue' | 'cyan' | 'dim'
|
|
3201
3152
|
|
|
3202
3153
|
type Channel = 'web' | 'json' | 'cli'
|
|
3203
3154
|
|
|
@@ -3409,8 +3360,6 @@ class StackChannel = {
|
|
|
3409
3360
|
|
|
3410
3361
|
const Log = LogManager
|
|
3411
3362
|
|
|
3412
|
-
function frameworkLog = (scope: string) => BoundLogger
|
|
3413
|
-
|
|
3414
3363
|
function LoggingConfig = (options?: Partial<LoggingConfigShape>) => LoggingConfigShape
|
|
3415
3364
|
|
|
3416
3365
|
function renderTable = (data: TableData, indent?: number) => string[]
|
|
@@ -3458,11 +3407,11 @@ interface LoggingConfigShape = {
|
|
|
3458
3407
|
slowQueryMs?: number
|
|
3459
3408
|
}
|
|
3460
3409
|
|
|
3461
|
-
type ChannelConfig = {
|
|
3410
|
+
type ChannelConfig = { driver: 'console'; level?: LogLevel; format?: 'json' | 'pretty' } | { driver: 'single'; level?: LogLevel; path: string } | { driver: 'daily'; level?: LogLevel; path: string; days?: number } | { driver: 'stack'; level?: LogLevel; channels: string[] } | { driver: 'null' }
|
|
3462
3411
|
|
|
3463
|
-
type LogLevel = '
|
|
3412
|
+
type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal'
|
|
3464
3413
|
|
|
3465
|
-
type TableData = Record<string, unknown> |
|
|
3414
|
+
type TableData = Record<string, unknown> | ReadonlyArray<Record<string, unknown>>
|
|
3466
3415
|
|
|
3467
3416
|
## ./macros/config `(./src/macros/config.macro.ts)`
|
|
3468
3417
|
|
|
@@ -3506,17 +3455,11 @@ function hasRoute = (name: string) => boolean
|
|
|
3506
3455
|
|
|
3507
3456
|
function resetRoutes = () => void
|
|
3508
3457
|
|
|
3509
|
-
function routeMethod = (name: string) => string | undefined
|
|
3510
|
-
|
|
3511
3458
|
interface RouteAction = {
|
|
3512
3459
|
method: string
|
|
3513
3460
|
url: string
|
|
3514
3461
|
}
|
|
3515
3462
|
|
|
3516
|
-
interface RouteMethodRegistry = {}
|
|
3517
|
-
|
|
3518
|
-
type MethodedRouteName = never
|
|
3519
|
-
|
|
3520
3463
|
type RouteArgs = [params?: RouteParamValues, query?: RouteQuery]
|
|
3521
3464
|
|
|
3522
3465
|
type RouteParamValues = { [x: string]: RouteParamValue | readonly RouteParamValue[];}
|
|
@@ -3564,7 +3507,7 @@ interface RedactGraphOptions = {
|
|
|
3564
3507
|
tooDeep: string
|
|
3565
3508
|
}
|
|
3566
3509
|
|
|
3567
|
-
type HashAlgorithm = '
|
|
3510
|
+
type HashAlgorithm = 'argon2id' | 'argon2i' | 'argon2d' | 'bcrypt'
|
|
3568
3511
|
|
|
3569
3512
|
## ./storage `(./src/storage/index.ts)`
|
|
3570
3513
|
|
package/package.json
CHANGED
|
@@ -34,7 +34,12 @@ import {
|
|
|
34
34
|
} from "../middleware/SecureHeadersMiddleware.ts";
|
|
35
35
|
import { isAllowedOrigin, allowedOriginsFrom } from "../http/originGuard.ts";
|
|
36
36
|
import { rescueSync } from "../helpers/index.ts";
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
configureAssets,
|
|
39
|
+
setAssetVersion,
|
|
40
|
+
assetVersion,
|
|
41
|
+
deriveAssetVersion,
|
|
42
|
+
} from "../assets/assets.ts";
|
|
38
43
|
import { ConfigLoader, type ConfigMap } from "../config/ConfigLoader.ts";
|
|
39
44
|
import { Emitter } from "../events/Emitter.ts";
|
|
40
45
|
import { FrameworkEvents, AppBooted } from "../events/FrameworkEvents.ts";
|
|
@@ -1358,7 +1363,18 @@ export class Application {
|
|
|
1358
1363
|
// each rebuild ships a fresh token over the reload channel (see ServeCommand).
|
|
1359
1364
|
const isDevWorker = process.argv.includes("--dev-worker");
|
|
1360
1365
|
configureAssets({ prefix: assets?.prefix ?? "/", dev: isDevWorker });
|
|
1361
|
-
|
|
1366
|
+
|
|
1367
|
+
if (isDevWorker) {
|
|
1368
|
+
setAssetVersion(Bun.env["ZT_ASSET_VERSION"] ?? "");
|
|
1369
|
+
} else {
|
|
1370
|
+
// Production gets a token too, derived from the built files. `assets:build`
|
|
1371
|
+
// writes `app.js` under that name every deploy and the static handler sends
|
|
1372
|
+
// no `Cache-Control`, so without this a returning visitor keeps the bundle
|
|
1373
|
+
// their browser cached — through any number of deploys, while the server
|
|
1374
|
+
// reports success. Derived rather than random so a restart that changed no
|
|
1375
|
+
// assets does not invalidate every client's cache for nothing.
|
|
1376
|
+
setAssetVersion(deriveAssetVersion(`${dir}/${assets?.outDir ?? "public"}`));
|
|
1377
|
+
}
|
|
1362
1378
|
}
|
|
1363
1379
|
|
|
1364
1380
|
// Convention phase — discovers app/schedules (class-based), models, observers, jobs, etc.
|
package/src/assets/assets.ts
CHANGED
|
@@ -14,8 +14,25 @@
|
|
|
14
14
|
* {@link bumpAssetVersion}) and propagated from the dev orchestrator to the
|
|
15
15
|
* server worker over its reload channel. This mirrors Inertia's asset-version
|
|
16
16
|
* model — one token per build, busting every asset at once.
|
|
17
|
+
*
|
|
18
|
+
* ## Production needs this more than dev does
|
|
19
|
+
*
|
|
20
|
+
* It used to be dev-only, on the reasoning that production assets would be
|
|
21
|
+
* content-hashed by something downstream. Nothing does: `assets:build` writes
|
|
22
|
+
* `public/js/app.js` under that exact name every time, the static handler sends
|
|
23
|
+
* no `Cache-Control`, and so a returning visitor keeps whatever bundle their
|
|
24
|
+
* browser heuristically cached — for the life of that heuristic, across any
|
|
25
|
+
* number of deploys. A shipped fix that reaches nobody who has visited before is
|
|
26
|
+
* the worst kind, because everything on the server says it worked.
|
|
27
|
+
*
|
|
28
|
+
* So the version applies whenever it is set, in any environment. In production
|
|
29
|
+
* it is derived from the built files themselves ({@link deriveAssetVersion}), so
|
|
30
|
+
* it changes when the assets change and a plain restart does not bust a thing.
|
|
17
31
|
*/
|
|
18
32
|
|
|
33
|
+
import { readdirSync, statSync } from "node:fs";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
|
|
19
36
|
let _prefix = "/";
|
|
20
37
|
let _dev = false;
|
|
21
38
|
let _version = "";
|
|
@@ -36,6 +53,41 @@ export function setAssetVersion(version: string): void {
|
|
|
36
53
|
_version = version;
|
|
37
54
|
}
|
|
38
55
|
|
|
56
|
+
/**
|
|
57
|
+
* A token derived from the built assets in `dir`, or `""` when there are none.
|
|
58
|
+
*
|
|
59
|
+
* Name, size and mtime of every file, hashed. Content would be stricter and costs
|
|
60
|
+
* a full read of every bundle at boot for a token that only has to *change* when
|
|
61
|
+
* the files do — a rebuild that produces identical bytes keeping its old token is
|
|
62
|
+
* the correct outcome either way.
|
|
63
|
+
*
|
|
64
|
+
* `""` when the directory is missing or empty, which leaves `asset()` emitting
|
|
65
|
+
* clean URLs rather than a token that means nothing.
|
|
66
|
+
*/
|
|
67
|
+
export function deriveAssetVersion(dir: string): string {
|
|
68
|
+
let entries: string[];
|
|
69
|
+
try {
|
|
70
|
+
entries = readdirSync(dir, { recursive: true }) as string[];
|
|
71
|
+
} catch {
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const parts: string[] = [];
|
|
76
|
+
for (const entry of entries.sort()) {
|
|
77
|
+
try {
|
|
78
|
+
const info = statSync(join(dir, entry));
|
|
79
|
+
if (!info.isFile()) continue;
|
|
80
|
+
parts.push(`${entry}:${info.size}:${Math.floor(info.mtimeMs)}`);
|
|
81
|
+
} catch {
|
|
82
|
+
// Raced with a deploy swapping the directory. Skipping the file is fine:
|
|
83
|
+
// whatever lands next boot produces a different token anyway.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (parts.length === 0) return "";
|
|
88
|
+
return Bun.hash(parts.join("|")).toString(36).slice(0, 10);
|
|
89
|
+
}
|
|
90
|
+
|
|
39
91
|
/** The current asset version, or `""` when unset. */
|
|
40
92
|
export function assetVersion(): string {
|
|
41
93
|
return _version;
|
|
@@ -56,15 +108,15 @@ export function bumpAssetVersion(): string {
|
|
|
56
108
|
* Build the public URL for a front-end asset.
|
|
57
109
|
*
|
|
58
110
|
* @param path Asset path relative to the asset prefix (leading slash optional).
|
|
59
|
-
* @returns `<prefix>/<path>`, with `?v=<version>` appended
|
|
111
|
+
* @returns `<prefix>/<path>`, with `?v=<version>` appended when a version is set.
|
|
60
112
|
*
|
|
61
113
|
* @example
|
|
62
|
-
* asset("/app.css") //
|
|
63
|
-
* asset("app.css") //
|
|
114
|
+
* asset("/app.css") // → "/app.css?v=lq3k7m"
|
|
115
|
+
* asset("app.css") // → "/app.css?v=lq3k7m"
|
|
64
116
|
*/
|
|
65
117
|
export function asset(path: string): string {
|
|
66
118
|
const url = _joinUrl(_prefix, path);
|
|
67
|
-
if (
|
|
119
|
+
if (_version) {
|
|
68
120
|
const separator = url.includes("?") ? "&" : "?";
|
|
69
121
|
return `${url}${separator}v=${_version}`;
|
|
70
122
|
}
|
package/src/assets/index.ts
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Front-end asset URL helper and asset versioning (the `@zerotal/core/assets`
|
|
3
|
-
* subpath). {@link asset} builds the public URL for a built asset
|
|
4
|
-
* `?v=<version>` cache-buster in
|
|
5
|
-
*
|
|
3
|
+
* subpath). {@link asset} builds the public URL for a built asset and appends a
|
|
4
|
+
* `?v=<version>` cache-buster — in every environment, not only in dev.
|
|
5
|
+
*
|
|
6
|
+
* Production needs it more: `assets:build` writes `app.js` under that name on
|
|
7
|
+
* every deploy and the static handler sends no `Cache-Control`, so an unbusted
|
|
8
|
+
* URL leaves a returning visitor on whatever bundle their browser cached. The
|
|
9
|
+
* production token is derived from the built files, so it moves when they do and
|
|
10
|
+
* a plain restart busts nothing.
|
|
6
11
|
*
|
|
7
12
|
* @example
|
|
8
13
|
* ```ts
|
|
9
14
|
* import { asset } from "@zerotal/core/assets";
|
|
10
15
|
*
|
|
11
|
-
* asset("/app.css"); //
|
|
16
|
+
* asset("/app.css"); // → "/app.css?v=lq3k7m"
|
|
12
17
|
* ```
|
|
13
18
|
*
|
|
14
19
|
* @packageDocumentation
|
|
@@ -113,6 +113,61 @@ function _checkErrorDiscipline(files: PackageFiles, packageName: string): Violat
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
/** Lint every package under `packagesDir` and return a report per package. */
|
|
116
|
+
/**
|
|
117
|
+
* A package below `stable` names the release by which its label is reviewed, and
|
|
118
|
+
* the review actually falls due.
|
|
119
|
+
*
|
|
120
|
+
* The support policy's own argument is that "an honest 'experimental' is useful
|
|
121
|
+
* once and corrosive indefinitely" — a package that has worn the label for a year
|
|
122
|
+
* is not being cautious, it is unowned. It then set a review release for `ai` and
|
|
123
|
+
* `arch` in a prose table, where nothing could enforce it: the version could sail
|
|
124
|
+
* past and the only consequence would be a sentence quietly becoming untrue.
|
|
125
|
+
*
|
|
126
|
+
* So the date lives in `package.json` beside the label it governs, and when the
|
|
127
|
+
* package's version reaches it, this fails until somebody performs the review —
|
|
128
|
+
* promote, keep with a new date and the reason, or withdraw.
|
|
129
|
+
*/
|
|
130
|
+
function _checkMaturityReview(files: PackageFiles): Violation[] {
|
|
131
|
+
const violations: Violation[] = [];
|
|
132
|
+
const pkg = files.packageJson;
|
|
133
|
+
if (!pkg) return violations;
|
|
134
|
+
|
|
135
|
+
const maturity = pkg["maturity"] as string | undefined;
|
|
136
|
+
if (!maturity || maturity === "stable") return violations;
|
|
137
|
+
|
|
138
|
+
const review = pkg["maturityReview"] as string | undefined;
|
|
139
|
+
if (!review) {
|
|
140
|
+
violations.push({
|
|
141
|
+
rule: "maturity-review",
|
|
142
|
+
severity: "medium",
|
|
143
|
+
message: `maturity '${maturity}' with no 'maturityReview' — name the release by which the label is reviewed`,
|
|
144
|
+
});
|
|
145
|
+
return violations;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const version = pkg["version"] as string | undefined;
|
|
149
|
+
if (version && _atOrPast(version, review)) {
|
|
150
|
+
violations.push({
|
|
151
|
+
rule: "maturity-review-due",
|
|
152
|
+
severity: "high",
|
|
153
|
+
message: `maturity '${maturity}' was to be reviewed by ${review} and this package is ${version} — promote it, withdraw it, or set a new 'maturityReview' with the reason in docs/support-policy.md`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return violations;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Whether `version` has reached `target`, comparing numerically per segment. */
|
|
160
|
+
function _atOrPast(version: string, target: string): boolean {
|
|
161
|
+
const a = version.split(".").map(Number);
|
|
162
|
+
const b = target.split(".").map(Number);
|
|
163
|
+
for (let i = 0; i < 3; i++) {
|
|
164
|
+
const left = a[i] ?? 0;
|
|
165
|
+
const right = b[i] ?? 0;
|
|
166
|
+
if (left !== right) return left > right;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
116
171
|
export async function lintPackages(packagesDir: string): Promise<PackageReport[]> {
|
|
117
172
|
const reports: PackageReport[] = [];
|
|
118
173
|
let entries: string[];
|
|
@@ -134,6 +189,7 @@ export async function lintPackages(packagesDir: string): Promise<PackageReport[]
|
|
|
134
189
|
..._checkTests(files),
|
|
135
190
|
..._checkPackaging(files),
|
|
136
191
|
..._checkErrorDiscipline(files, packageName),
|
|
192
|
+
..._checkMaturityReview(files),
|
|
137
193
|
];
|
|
138
194
|
reports.push({ package: packageName, violations });
|
|
139
195
|
}
|
|
@@ -35,7 +35,8 @@ function _kindOf(ctor: EventCtor): string {
|
|
|
35
35
|
*
|
|
36
36
|
* @example
|
|
37
37
|
* ```ts
|
|
38
|
-
* import { FrameworkEvents
|
|
38
|
+
* import { FrameworkEvents } from "@zerotal/core";
|
|
39
|
+
* import { QueryExecuted } from "@zerotal/orm";
|
|
39
40
|
*
|
|
40
41
|
* // In a provider's onBooting(): watch every SQL query.
|
|
41
42
|
* const off = FrameworkEvents.on(QueryExecuted, (e) => {
|