@vzn/vx 0.0.645 → 0.0.648
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/README.md +39 -50
- package/launcher.cjs +9 -2
- package/launcher.sh +2 -2
- package/package.json +5 -5
- package/schemas/cache.json +5 -5
- package/schemas/error.json +1 -1
- package/schemas/info.json +16 -2
- package/schemas/last.json +127 -5
- package/schemas/plan.json +28 -2
- package/schemas/show.json +10 -1
- package/skills/vx/SKILL.md +19 -3
- package/src/bin.ts +54 -13
- package/src/cache/archive.ts +324 -23
- package/src/cache/cache.ts +330 -65
- package/src/cache/git-inputs.ts +274 -97
- package/src/cache/index.ts +7 -2
- package/src/cache/inputs.ts +515 -83
- package/src/cache/key-fold.ts +15 -6
- package/src/cache/layer.ts +74 -11
- package/src/cache/layered-cache.ts +154 -59
- package/src/cache/linked-inputs.ts +1 -1
- package/src/cache/output-index.ts +44 -14
- package/src/cache/plugin-cache.ts +49 -23
- package/src/cache/remote-hits.ts +72 -0
- package/src/cache/remote-lane.ts +100 -15
- package/src/cache/run-history.ts +77 -23
- package/src/cache/run-store.ts +11 -10
- package/src/cache/schema.ts +42 -6
- package/src/cache/tar-stream.ts +5 -0
- package/src/cache/umask.ts +1 -2
- package/src/cli/cache.ts +58 -27
- package/src/cli/completions.ts +12 -5
- package/src/cli/core-alias.ts +5 -2
- package/src/cli/docs.ts +42 -16
- package/src/cli/help.ts +62 -25
- package/src/cli/index.ts +79 -45
- package/src/cli/info.ts +12 -6
- package/src/cli/init.ts +222 -34
- package/src/cli/last.ts +443 -47
- package/src/cli/lock.ts +197 -52
- package/src/cli/plan-format.ts +91 -49
- package/src/cli/plugin-commands.ts +160 -11
- package/src/cli/plugin-templates.ts +13 -13
- package/src/cli/registry.ts +209 -0
- package/src/cli/run.ts +111 -41
- package/src/cli/select.ts +148 -49
- package/src/cli/show.ts +59 -16
- package/src/cli/task-verb.ts +16 -6
- package/src/cli/upgrade.ts +155 -54
- package/src/cli/watch-cycle.ts +10 -5
- package/src/cli/watch-filter.ts +71 -3
- package/src/cli/watch-fs.ts +22 -6
- package/src/cli/watch-judge.ts +48 -6
- package/src/cli/watch.ts +163 -42
- package/src/cli/why.ts +211 -73
- package/src/cli/workspace-config.ts +54 -15
- package/src/config.ts +13 -1
- package/src/exec/env.ts +7 -2
- package/src/exec/executor.ts +262 -15
- package/src/exec/index.ts +8 -2
- package/src/exec/kill-tree.ts +15 -0
- package/src/exec/local-executor.ts +1 -1
- package/src/exec/proc-sample.ts +70 -6
- package/src/exec/runner.ts +256 -53
- package/src/exec/sandbox-binds.ts +7 -3
- package/src/exec/sandbox-paths.ts +1 -1
- package/src/exec/sandbox-runtime.ts +222 -28
- package/src/exec/sandbox-violations.ts +52 -20
- package/src/graph/critical-path.ts +79 -0
- package/src/graph/default-build.ts +19 -0
- package/src/graph/definition.ts +38 -0
- package/src/graph/folds.ts +38 -5
- package/src/graph/index.ts +4 -1
- package/src/graph/priorities.ts +74 -8
- package/src/graph/scheduler.ts +326 -65
- package/src/graph/task-graph.ts +228 -35
- package/src/handoff.ts +97 -9
- package/src/history/failure-mode.ts +6 -3
- package/src/history/history.ts +268 -50
- package/src/history/index.ts +2 -0
- package/src/history/metrics.ts +142 -45
- package/src/index.ts +42 -10
- package/src/orchestrator/admission.ts +10 -6
- package/src/orchestrator/affected-tasks.ts +11 -6
- package/src/orchestrator/deferred-outputs.ts +2 -0
- package/src/orchestrator/doctor.ts +134 -42
- package/src/orchestrator/execute-args.ts +6 -0
- package/src/orchestrator/execute-task.ts +155 -42
- package/src/orchestrator/fingerprint-watch.ts +1 -1
- package/src/orchestrator/graph-memo.ts +140 -0
- package/src/orchestrator/graph-stage.ts +18 -1
- package/src/orchestrator/held-servers.ts +86 -0
- package/src/orchestrator/hit-restore.ts +227 -122
- package/src/orchestrator/index.ts +20 -3
- package/src/orchestrator/local-floor.ts +198 -0
- package/src/orchestrator/local-shortcircuit.ts +51 -8
- package/src/orchestrator/lockfile-claim.ts +96 -16
- package/src/orchestrator/miss-save.ts +52 -12
- package/src/orchestrator/options.ts +8 -6
- package/src/orchestrator/output-host.ts +43 -14
- package/src/orchestrator/output-log.ts +90 -10
- package/src/orchestrator/persistent-task.ts +97 -28
- package/src/orchestrator/persistent.ts +16 -4
- package/src/orchestrator/placement.ts +257 -14
- package/src/orchestrator/plan.ts +64 -7
- package/src/orchestrator/plugin-host.ts +230 -40
- package/src/orchestrator/plugin.ts +150 -37
- package/src/orchestrator/prepare.ts +178 -32
- package/src/orchestrator/projects.ts +65 -40
- package/src/orchestrator/remote-prefetch.ts +119 -25
- package/src/orchestrator/run-artifacts.ts +95 -32
- package/src/orchestrator/run-context.ts +39 -41
- package/src/orchestrator/run-failures.ts +65 -23
- package/src/orchestrator/run-id.ts +6 -3
- package/src/orchestrator/run-keys.ts +300 -0
- package/src/orchestrator/run-live.ts +294 -0
- package/src/orchestrator/run-lock.ts +13 -7
- package/src/orchestrator/run-records.ts +35 -23
- package/src/orchestrator/run-report.ts +3 -3
- package/src/orchestrator/run-steps.ts +58 -22
- package/src/orchestrator/run-stop.ts +22 -5
- package/src/orchestrator/run.ts +409 -79
- package/src/orchestrator/sandbox-fix.ts +7 -5
- package/src/orchestrator/sandbox-request.ts +78 -22
- package/src/orchestrator/shell-verdict.ts +5 -5
- package/src/orchestrator/signals.ts +26 -1
- package/src/orchestrator/stable-keys.ts +92 -8
- package/src/orchestrator/steps.ts +2 -2
- package/src/orchestrator/task-artifacts.ts +206 -0
- package/src/orchestrator/task-hash.ts +89 -14
- package/src/orchestrator/task-log-buffer.ts +72 -36
- package/src/orchestrator/task-process.ts +22 -0
- package/src/orchestrator/telemetry-host.ts +24 -3
- package/src/orchestrator/telemetry.ts +113 -38
- package/src/orchestrator/upstream.ts +15 -0
- package/src/report/events.ts +32 -13
- package/src/report/forecast.ts +36 -35
- package/src/report/framed-output.ts +173 -46
- package/src/report/index.ts +1 -0
- package/src/report/logger.ts +217 -186
- package/src/report/path-links.ts +12 -4
- package/src/report/plain-output.ts +13 -5
- package/src/report/status-line.ts +94 -46
- package/src/report/summary.ts +174 -38
- package/src/report/terminal-signals.ts +13 -2
- package/src/settings.ts +14 -6
- package/src/util/bun-version.ts +4 -1
- package/src/util/colors.ts +20 -3
- package/src/util/config-frame.ts +41 -4
- package/src/{graph → util}/dependency-spec.ts +22 -13
- package/src/util/edit-distance.ts +13 -1
- package/src/util/env-flag.ts +9 -0
- package/src/util/errors.ts +59 -3
- package/src/util/essential-env.ts +57 -0
- package/src/util/git-ignored.ts +1 -1
- package/src/util/hash.ts +35 -0
- package/src/util/hide-env.ts +45 -5
- package/src/util/index.ts +22 -5
- package/src/util/num.ts +8 -0
- package/src/util/paths.ts +121 -21
- package/src/util/report.ts +6 -1
- package/src/util/secret-mask.ts +62 -1
- package/src/util/secrets.ts +7 -2
- package/src/util/size.ts +11 -3
- package/src/util/tail.ts +6 -1
- package/src/util/timing.ts +13 -1
- package/src/util/ui.ts +65 -4
- package/src/util/which.ts +1 -1
- package/src/util/write-file.ts +67 -0
- package/src/workspace/affected.ts +710 -107
- package/src/workspace/bracket-literal.ts +84 -0
- package/src/workspace/canonical-dir.ts +37 -0
- package/src/workspace/config-bytecode.ts +282 -0
- package/src/workspace/config-eval.ts +29 -8
- package/src/workspace/config-imports.ts +199 -58
- package/src/workspace/config-schema.ts +242 -39
- package/src/workspace/filter.ts +37 -8
- package/src/workspace/index.ts +11 -2
- package/src/workspace/json-data.ts +3 -4
- package/src/workspace/lockfile.ts +57 -8
- package/src/workspace/migration.ts +72 -49
- package/src/workspace/package-graph.ts +5 -4
- package/src/workspace/project-loader.ts +314 -47
- package/src/workspace/repo-id.ts +1 -1
- package/src/workspace/workspace.ts +176 -33
- package/src/cli/foreign-flags.ts +0 -727
|
@@ -20,10 +20,20 @@ import {
|
|
|
20
20
|
import { PLUGIN_PACKAGE, type ProjectConfig, type WorkspaceConfig } from '../config.js'
|
|
21
21
|
import type { TaskExecutor } from '../exec/index.js'
|
|
22
22
|
import type { TaskNode, TaskOutcome, TaskStatus } from '../graph/index.js'
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
errorCode,
|
|
25
|
+
maskedLine,
|
|
26
|
+
nearest,
|
|
27
|
+
type PluginSecrets,
|
|
28
|
+
SecretBroker,
|
|
29
|
+
thrownMessage,
|
|
30
|
+
ui,
|
|
31
|
+
UserError,
|
|
32
|
+
} from '../util/index.js'
|
|
24
33
|
import type { ProjectMeta } from '../workspace/index.js'
|
|
25
34
|
import type { EventBus, Logger, RunStartInfo } from '../report/index.js'
|
|
26
35
|
import type { CiContext } from './run-context.js'
|
|
36
|
+
import type { CacheFacts, TaskArtifacts } from './task-artifacts.js'
|
|
27
37
|
import type { TelemetryContext, TelemetrySink } from './telemetry.js'
|
|
28
38
|
|
|
29
39
|
/**
|
|
@@ -59,7 +69,8 @@ export interface VxPlugin {
|
|
|
59
69
|
|
|
60
70
|
/**
|
|
61
71
|
* The workspace config, before anything is derived from it (concurrency,
|
|
62
|
-
* cacheDir, timeout, …). `plugins` is already fixed by the time this runs
|
|
72
|
+
* cacheDir, timeout, …). `plugins` is already fixed by the time this runs:
|
|
73
|
+
* an edit to it is refused.
|
|
63
74
|
* Core re-validates after each plugin, as it does a user's file.
|
|
64
75
|
* `ctx.setBranch` names what the CI knows of the branch: its cache scope
|
|
65
76
|
* and the commit it is compared with.
|
|
@@ -77,7 +88,7 @@ export interface VxPlugin {
|
|
|
77
88
|
* (with no config; named by `name`, else its path). A project named by
|
|
78
89
|
* its path takes `name`; one its config names keeps that name, and a
|
|
79
90
|
* different name from an earlier plugin is refused, as is a directory
|
|
80
|
-
* outside the root. Two projects of one name are refused once the stage
|
|
91
|
+
* outside the root and the name `//` away from it. Two projects of one name are refused once the stage
|
|
81
92
|
* ends. `deps` and `reach` from every plugin union then; a target that is
|
|
82
93
|
* no project is refused.
|
|
83
94
|
*/
|
|
@@ -92,7 +103,7 @@ export interface VxPlugin {
|
|
|
92
103
|
* applied on top of it live. A project with NO config file (a
|
|
93
104
|
* `discover` plugin added it) is loaded too, as `{ tasks: {} }`, so a
|
|
94
105
|
* plugin can give it tasks; with no `project` plugin such a project is
|
|
95
|
-
* never visited.
|
|
106
|
+
* never visited. Its `name` is fixed by then: a change is refused.
|
|
96
107
|
*/
|
|
97
108
|
project?(config: ProjectConfig, ctx: ProjectHookContext): void | Promise<void>
|
|
98
109
|
|
|
@@ -153,13 +164,13 @@ export interface VxPlugin {
|
|
|
153
164
|
* on this machine, with the tasks running here right now. Return `false`
|
|
154
165
|
* to hold it until something finishes (it is asked again then). Asked
|
|
155
166
|
* many times per run, so it must be cheap and synchronous; a throw, or
|
|
156
|
-
* an answer that is a Promise, is reported once and the plugin admits
|
|
167
|
+
* an answer that is a Promise or not a boolean or undefined, is reported once and the plugin admits
|
|
157
168
|
* from then on — a policy never
|
|
158
169
|
* breaks a run. Restore-tier hits, tasks on an executor pool and groups
|
|
159
170
|
* hold no local resources: never asked, never in `ctx.running`. When
|
|
160
171
|
* several plugins answer, all must admit. Core keeps no notion of what a task needs: a plugin that
|
|
161
172
|
* packs memory or CPU learns or declares the numbers itself
|
|
162
|
-
* (`@vzn/vx-
|
|
173
|
+
* (`@vzn/vx-history` packs what past executions used).
|
|
163
174
|
*/
|
|
164
175
|
admit?(task: TaskNode, ctx: AdmitContext): boolean
|
|
165
176
|
|
|
@@ -222,8 +233,10 @@ export interface VxPlugin {
|
|
|
222
233
|
* (the contract `run({ log })` takes), or `undefined` to leave core's
|
|
223
234
|
* renderer in place. The first plugin, in declaration order, that
|
|
224
235
|
* returns one renders the run; the rest are not asked. Not consulted
|
|
225
|
-
* when an embedder passed its own `log`. A logger that throws
|
|
226
|
-
* rest of the run back to core's renderer with one warning
|
|
236
|
+
* when an embedder passed its own `log`. A logger that throws or rejects
|
|
237
|
+
* hands the rest of the run back to core's renderer with one warning; a
|
|
238
|
+
* hook that does not answer within the teardown bound (3 s) is passed
|
|
239
|
+
* over with one.
|
|
227
240
|
*/
|
|
228
241
|
output?(ctx: OutputContext): Logger | undefined | Promise<Logger | undefined>
|
|
229
242
|
|
|
@@ -239,8 +252,10 @@ export interface VxPlugin {
|
|
|
239
252
|
setup?(ctx: PluginSetupContext): void | Promise<void>
|
|
240
253
|
|
|
241
254
|
/**
|
|
242
|
-
* Optional teardown at end-of-run (flush a sink, close a socket)
|
|
243
|
-
*
|
|
255
|
+
* Optional teardown at end-of-run (flush a sink, close a socket), in
|
|
256
|
+
* declaration order, a plugin with `cache` after every other: the
|
|
257
|
+
* others may still write through its remote. Errors are logged, never
|
|
258
|
+
* thrown. A run that ran to its end passes its
|
|
244
259
|
* `RunReport`; every other exit (a refused setup, a stop) passes none.
|
|
245
260
|
*/
|
|
246
261
|
teardown?(run?: RunReport): void | Promise<void>
|
|
@@ -334,6 +349,20 @@ export interface CommandContext extends BaseContext {
|
|
|
334
349
|
* workspace's cache plugins supply, under the run's scope. Opened on first use.
|
|
335
350
|
*/
|
|
336
351
|
readonly cache: PluginCache
|
|
352
|
+
/**
|
|
353
|
+
* Task entries by key in the workspace's cache, under a run's policy and
|
|
354
|
+
* scope (a laptop writes no remote unless the workspace's policy names
|
|
355
|
+
* it): what a verb that runs a run's tasks on this machine saves and
|
|
356
|
+
* restores through (`TaskArtifacts`). Opened on first use.
|
|
357
|
+
*/
|
|
358
|
+
readonly artifacts: TaskArtifacts
|
|
359
|
+
/**
|
|
360
|
+
* `artifacts` read as another machine's run would read them: under its
|
|
361
|
+
* refs, with the access its remotes lent (`CacheFacts`, asked on each
|
|
362
|
+
* request, so a token it lends again replaces the last). Opened on first
|
|
363
|
+
* use; until `facts` answers, under this machine's own.
|
|
364
|
+
*/
|
|
365
|
+
artifactsFor(facts: () => CacheFacts | undefined): TaskArtifacts
|
|
337
366
|
}
|
|
338
367
|
|
|
339
368
|
/** `config` runs before the cache dir is known — it may be what the hook changes. */
|
|
@@ -343,17 +372,22 @@ export interface WorkspaceHookContext {
|
|
|
343
372
|
}
|
|
344
373
|
|
|
345
374
|
/**
|
|
346
|
-
* What a CI knows of the run's branch (
|
|
375
|
+
* What a CI knows of the run's branch (2026-10-17): `scope`, the
|
|
347
376
|
* cache scope (`'vznjs/vx:feat-x'`; `VX_CACHE_SCOPE` beats it, and it
|
|
348
377
|
* beats the one git names), and `base`, the commit the branch is compared
|
|
349
378
|
* with (a pull request's base), for a bare `--affected`. `trusted`: the
|
|
350
|
-
* run has no scope, whatever git names (a merge queue's run,
|
|
351
|
-
* 2026-10-18). The scope keys plugin entries only.
|
|
379
|
+
* run has no scope, whatever git names (a merge queue's run,
|
|
380
|
+
* 2026-10-18). The scope keys plugin entries only. `refs`: the refs a
|
|
381
|
+
* remote reads, in order (`CacheContext.refs`; 2026-10-25: a pull
|
|
382
|
+
* request its head branch, then its base, then the default branch; a
|
|
383
|
+
* merge queue or train only its base); unset, git's. A later plugin's
|
|
384
|
+
* field wins.
|
|
352
385
|
*/
|
|
353
386
|
export interface BranchFacts {
|
|
354
387
|
scope?: string
|
|
355
388
|
base?: string
|
|
356
389
|
trusted?: boolean
|
|
390
|
+
refs?: readonly string[]
|
|
357
391
|
}
|
|
358
392
|
|
|
359
393
|
export interface ConfigContext extends WorkspaceHookContext {
|
|
@@ -437,7 +471,7 @@ export interface FingerprintClaim {
|
|
|
437
471
|
* by `--affected` with the file's bytes at the base ref and in the
|
|
438
472
|
* working tree (`null` where it does not exist). Return `undefined` to
|
|
439
473
|
* select every project — the answer a claimant gives when it cannot
|
|
440
|
-
* tell.
|
|
474
|
+
* tell. A name that is not one of `ctx.projects` is refused.
|
|
441
475
|
*/
|
|
442
476
|
affected(
|
|
443
477
|
change: FingerprintChange,
|
|
@@ -449,9 +483,14 @@ export interface FingerprintClaim {
|
|
|
449
483
|
* path and hashes nowhere, or a `file:` dependency's directory. A change
|
|
450
484
|
* to one, or under one, asks `affected` about the file naming it, with
|
|
451
485
|
* `change.extras`. Without it, a `bun.lock` patch edit selects every
|
|
452
|
-
* project.
|
|
486
|
+
* project. Asked on every `--affected` diff, so a claimant that memoises
|
|
487
|
+
* its parse answers from `ctx.cacheDir`.
|
|
453
488
|
*/
|
|
454
|
-
extraFiles?(
|
|
489
|
+
extraFiles?(
|
|
490
|
+
file: string,
|
|
491
|
+
bytes: Uint8Array,
|
|
492
|
+
ctx: FingerprintContext,
|
|
493
|
+
): readonly string[] | Promise<readonly string[]>
|
|
455
494
|
}
|
|
456
495
|
|
|
457
496
|
export interface FingerprintChange {
|
|
@@ -492,14 +531,33 @@ export interface ScheduleHookContext extends BaseContext {
|
|
|
492
531
|
readonly localCache: Cache
|
|
493
532
|
/** The run's branch (`currentBranch`), the timings a forecast reads first: pass it to `LocalHistoryProvider`. */
|
|
494
533
|
readonly branch: string | null
|
|
534
|
+
/** The refs the run reads, in order (`CacheContext.refs`): a remote's timings are read by them too. */
|
|
535
|
+
readonly refs: readonly string[]
|
|
495
536
|
/** This plugin's entries in the workspace's cache, as `setup` has them. */
|
|
496
537
|
readonly cache: PluginCache
|
|
497
538
|
/**
|
|
498
539
|
* Replace this plugin's weights while the run goes on: the tasks not yet
|
|
499
540
|
* started re-rank at once. For weights that arrive late (timings fetched
|
|
500
541
|
* from a remote). Ignored in a plan and once the run's tasks are done.
|
|
542
|
+
* Given a promise, the weights it resolves to, if any, when it does:
|
|
543
|
+
* `refining()` waits for it meanwhile.
|
|
544
|
+
*/
|
|
545
|
+
refine(
|
|
546
|
+
weights: ReadonlyMap<string, number> | Promise<ReadonlyMap<string, number> | undefined>,
|
|
547
|
+
): void
|
|
548
|
+
/**
|
|
549
|
+
* Settles once every refine promised so far (a promise to `refine`) has
|
|
550
|
+
* landed or failed, its listeners called: a plugin that sizes the run by
|
|
551
|
+
* the fetched data waits for it here rather than guessing without it.
|
|
552
|
+
*/
|
|
553
|
+
refining(): Promise<void>
|
|
554
|
+
/**
|
|
555
|
+
* Calls `listener` each time another plugin `refine`s: the late data it
|
|
556
|
+
* waited for (timings fetched from a remote) has landed in history, so a
|
|
557
|
+
* plugin that read history in `schedule` may read it again. Never called
|
|
558
|
+
* in a plan.
|
|
501
559
|
*/
|
|
502
|
-
|
|
560
|
+
onRefine(listener: () => void): void
|
|
503
561
|
}
|
|
504
562
|
|
|
505
563
|
/** A `cache` hook's context: the workspace and the run's scope, never the local store or the policy, which core owns. */
|
|
@@ -515,6 +573,13 @@ export interface CacheContext extends BaseContext {
|
|
|
515
573
|
* keys or scopes its entries by them; core keys task entries by none.
|
|
516
574
|
*/
|
|
517
575
|
readonly refs: readonly string[]
|
|
576
|
+
/**
|
|
577
|
+
* The token a run lent this plugin's remote (`RemoteCacheLayer.lend`),
|
|
578
|
+
* on a machine that runs that run's tasks (`CommandContext.artifactsFor`);
|
|
579
|
+
* undefined anywhere else. Read it per request: a run lends again before
|
|
580
|
+
* a token ends. A plugin uses it only when it has no access of its own.
|
|
581
|
+
*/
|
|
582
|
+
readonly lent?: () => string | undefined
|
|
518
583
|
}
|
|
519
584
|
|
|
520
585
|
export interface OutputContext extends BaseContext {
|
|
@@ -527,6 +592,14 @@ export interface OutputContext extends BaseContext {
|
|
|
527
592
|
export interface ExecutorContext extends BaseContext {
|
|
528
593
|
/** The run's worker count — an executor that paces itself reads it here. */
|
|
529
594
|
readonly concurrency: number
|
|
595
|
+
/** Task entries by key in the run's cache, for a task another machine ran (`TaskArtifacts`). */
|
|
596
|
+
readonly artifacts: TaskArtifacts
|
|
597
|
+
/**
|
|
598
|
+
* What a machine that runs this run's tasks reads the cache under
|
|
599
|
+
* (`CacheFacts`): the run's refs, and each remote's lent access, asked
|
|
600
|
+
* now. Ask again before `expiresAt`.
|
|
601
|
+
*/
|
|
602
|
+
cacheFacts(): Promise<CacheFacts>
|
|
530
603
|
}
|
|
531
604
|
|
|
532
605
|
export interface PluginContext extends PluginSetupContext {
|
|
@@ -569,7 +642,7 @@ export interface PluginHookHandlers {
|
|
|
569
642
|
}
|
|
570
643
|
|
|
571
644
|
export interface Plugin {
|
|
572
|
-
/**
|
|
645
|
+
/** The plugin's package name (`definePlugin` sets it), logged on errors. */
|
|
573
646
|
readonly name: string
|
|
574
647
|
/**
|
|
575
648
|
* Called once at the start of every run. Register hooks via
|
|
@@ -598,10 +671,15 @@ export interface PluginOrigin {
|
|
|
598
671
|
|
|
599
672
|
const packageNameByDir = new Map<string, string>()
|
|
600
673
|
|
|
601
|
-
/** The name of the nearest `package.json` above `dir` — the one that owns it. */
|
|
674
|
+
/** The name of the nearest named `package.json` above `dir` — the one that owns it. */
|
|
602
675
|
function pluginPackageName(dir: string): string {
|
|
603
676
|
const memo = packageNameByDir.get(dir)
|
|
604
677
|
if (memo !== undefined) return memo
|
|
678
|
+
// A package.json with no `name` below the package root is no package:
|
|
679
|
+
// dual builds (tshy) write `{ "type": "module" }` into `dist/esm/`. The
|
|
680
|
+
// walk passes it, as Vite's package lookup does; with no named one
|
|
681
|
+
// above, the nearest is refused.
|
|
682
|
+
let nameless: string | undefined
|
|
605
683
|
for (let d = dir; ;) {
|
|
606
684
|
let text: string | undefined
|
|
607
685
|
try {
|
|
@@ -622,18 +700,22 @@ function pluginPackageName(dir: string): string {
|
|
|
622
700
|
)
|
|
623
701
|
}
|
|
624
702
|
const name = pkg?.name
|
|
625
|
-
if (
|
|
703
|
+
if (name === undefined) nameless ??= path.join(d, 'package.json')
|
|
704
|
+
else if (typeof name !== 'string' || name.length === 0) {
|
|
626
705
|
throw new UserError(
|
|
627
706
|
`definePlugin: ${path.join(d, 'package.json')} has no name — a plugin is a package, and its name is the package's`,
|
|
628
707
|
)
|
|
708
|
+
} else {
|
|
709
|
+
packageNameByDir.set(dir, name)
|
|
710
|
+
return name
|
|
629
711
|
}
|
|
630
|
-
packageNameByDir.set(dir, name)
|
|
631
|
-
return name
|
|
632
712
|
}
|
|
633
713
|
const parent = path.dirname(d)
|
|
634
714
|
if (parent === d) {
|
|
635
715
|
throw new UserError(
|
|
636
|
-
|
|
716
|
+
nameless === undefined
|
|
717
|
+
? `definePlugin: no package.json above ${dir} — a plugin is a package, and its name is the package's`
|
|
718
|
+
: `definePlugin: ${nameless} has no name — a plugin is a package, and its name is the package's`,
|
|
637
719
|
)
|
|
638
720
|
}
|
|
639
721
|
d = parent
|
|
@@ -643,10 +725,24 @@ function pluginPackageName(dir: string): string {
|
|
|
643
725
|
/**
|
|
644
726
|
* The one way to make a plugin: `definePlugin(import.meta, { ...hooks })`.
|
|
645
727
|
* The name is read from the package the calling module belongs to — the
|
|
646
|
-
* nearest `package.json` above it — and stamped where the workspace loader
|
|
728
|
+
* nearest `package.json` above it that has a name — and stamped where the workspace loader
|
|
647
729
|
* checks for it, so a plugin cannot be named anything but its package.
|
|
648
730
|
*/
|
|
649
731
|
export function definePlugin(origin: PluginOrigin, hooks: PluginHooks): VxPlugin {
|
|
732
|
+
// The hooks are copied by spread, which takes own fields only: a class
|
|
733
|
+
// instance's methods stayed on its prototype and the plugin loaded
|
|
734
|
+
// without them, saying nothing.
|
|
735
|
+
const proto: unknown =
|
|
736
|
+
hooks !== null && typeof hooks === 'object' ? Object.getPrototypeOf(hooks) : undefined
|
|
737
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
738
|
+
const got =
|
|
739
|
+
hooks === null || typeof hooks !== 'object'
|
|
740
|
+
? String(hooks)
|
|
741
|
+
: `a ${(hooks as object).constructor?.name || 'class instance'}`
|
|
742
|
+
throw new UserError(
|
|
743
|
+
`definePlugin: the hooks must be a plain object ({ setup() { … } }), got ${got}`,
|
|
744
|
+
)
|
|
745
|
+
}
|
|
650
746
|
if ('name' in hooks) {
|
|
651
747
|
throw new UserError(`definePlugin: a plugin's name is its package name — drop the 'name' field`)
|
|
652
748
|
}
|
|
@@ -668,9 +764,11 @@ type OptionKind<V> = [V] extends [string]
|
|
|
668
764
|
? 'boolean'
|
|
669
765
|
: [V] extends [(...args: never[]) => unknown]
|
|
670
766
|
? 'function'
|
|
671
|
-
: [V] extends [
|
|
672
|
-
? '
|
|
673
|
-
:
|
|
767
|
+
: [V] extends [readonly unknown[]]
|
|
768
|
+
? 'array'
|
|
769
|
+
: [V] extends [object]
|
|
770
|
+
? 'object'
|
|
771
|
+
: 'any'
|
|
674
772
|
|
|
675
773
|
/**
|
|
676
774
|
* Every option a factory takes, each with the one kind its type allows
|
|
@@ -702,7 +800,8 @@ export function refuseUnknownOptions<T>(
|
|
|
702
800
|
const known = kinds as Readonly<Record<string, string>>
|
|
703
801
|
const names = Object.keys(known)
|
|
704
802
|
for (const [key, value] of Object.entries(options)) {
|
|
705
|
-
|
|
803
|
+
// Own keys only: the record's prototype answers `constructor`.
|
|
804
|
+
const kind = Object.hasOwn(known, key) ? known[key] : undefined
|
|
706
805
|
if (kind === undefined) {
|
|
707
806
|
const near = nearest(key, names)
|
|
708
807
|
throw new UserError(
|
|
@@ -714,12 +813,14 @@ export function refuseUnknownOptions<T>(
|
|
|
714
813
|
const ok =
|
|
715
814
|
kind === 'object'
|
|
716
815
|
? value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
717
|
-
:
|
|
816
|
+
: kind === 'array'
|
|
817
|
+
? Array.isArray(value)
|
|
818
|
+
: typeof value === kind
|
|
718
819
|
if (!ok) {
|
|
719
820
|
const got =
|
|
720
821
|
typeof value === 'function' ? 'a function' : (JSON.stringify(value) ?? String(value))
|
|
721
822
|
throw new UserError(
|
|
722
|
-
`${factory} option "${key}" must be ${kind === 'object' ? 'an' : 'a'} ${kind}, got ${got}`,
|
|
823
|
+
`${factory} option "${key}" must be ${kind === 'object' || kind === 'array' ? 'an' : 'a'} ${kind}, got ${got}`,
|
|
723
824
|
)
|
|
724
825
|
}
|
|
725
826
|
}
|
|
@@ -731,7 +832,7 @@ export interface InstallPluginsArgs {
|
|
|
731
832
|
cacheDir: string
|
|
732
833
|
bus: EventBus
|
|
733
834
|
/**
|
|
734
|
-
* Where plugin-throw warnings go. Defaults to
|
|
835
|
+
* Where plugin-throw warnings go. Defaults to `ui.say` on stderr; passing
|
|
735
836
|
* a callback lets the orchestrator funnel into the framed-output
|
|
736
837
|
* `run:status` channel.
|
|
737
838
|
*/
|
|
@@ -757,8 +858,9 @@ export class PluginSetupError extends UserError {
|
|
|
757
858
|
constructor(
|
|
758
859
|
readonly plugin: Plugin,
|
|
759
860
|
message: string,
|
|
861
|
+
code = 'VX_E_REFUSED',
|
|
760
862
|
) {
|
|
761
|
-
super(message)
|
|
863
|
+
super(message, code)
|
|
762
864
|
}
|
|
763
865
|
}
|
|
764
866
|
|
|
@@ -774,7 +876,7 @@ export class PluginSetupError extends UserError {
|
|
|
774
876
|
*/
|
|
775
877
|
export async function installPlugins(args: InstallPluginsArgs): Promise<() => void> {
|
|
776
878
|
const { plugins, bus, workspaceRoot, cacheDir } = args
|
|
777
|
-
const warn = args.warn ?? ((m) =>
|
|
879
|
+
const warn = args.warn ?? ((m) => ui.say('warn', m))
|
|
778
880
|
const secrets = args.secrets ?? new SecretBroker(args.ci ?? false, warn)
|
|
779
881
|
const disposers: Array<() => void> = []
|
|
780
882
|
// By object, not name: one package can export two plugins.
|
|
@@ -796,7 +898,7 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
796
898
|
if (disabled.has(plugin)) return
|
|
797
899
|
disabled.add(plugin)
|
|
798
900
|
warn(
|
|
799
|
-
`[vx] plugin '${plugin.name}' threw in ${where}; disabled for this run: ${
|
|
901
|
+
`[vx] plugin '${plugin.name}' threw in ${where}; disabled for this run: ${thrownMessage(err)}`,
|
|
800
902
|
)
|
|
801
903
|
}
|
|
802
904
|
const ctx: PluginContext = {
|
|
@@ -813,6 +915,11 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
813
915
|
bus: {
|
|
814
916
|
emit: (event) => bus.emit(event),
|
|
815
917
|
subscribe(subscriber) {
|
|
918
|
+
if (typeof subscriber !== 'function') {
|
|
919
|
+
throw new Error(
|
|
920
|
+
`ctx.bus.subscribe: the subscriber is ${subscriber === null ? 'null' : typeof subscriber}, not a function`,
|
|
921
|
+
)
|
|
922
|
+
}
|
|
816
923
|
// Isolated like an `on` handler: the bus itself swallows a throw.
|
|
817
924
|
const dispose = bus.subscribe((event) => {
|
|
818
925
|
if (disabled.has(plugin)) return
|
|
@@ -837,6 +944,12 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
837
944
|
`ctx.on: unknown hook '${String(hook)}' (one of ${Object.keys(HOOK_NAMES).join(', ')})`,
|
|
838
945
|
)
|
|
839
946
|
}
|
|
947
|
+
// Taken, it threw at the first event and switched the whole plugin off.
|
|
948
|
+
if (typeof handler !== 'function') {
|
|
949
|
+
throw new Error(
|
|
950
|
+
`ctx.on: the handler for '${hook}' is ${handler === null ? 'null' : typeof handler}, not a function`,
|
|
951
|
+
)
|
|
952
|
+
}
|
|
840
953
|
const onFail = (err: unknown): void => fail(hook, err)
|
|
841
954
|
const dispose = bus.subscribe((event) => {
|
|
842
955
|
if (disabled.has(plugin)) return
|
|
@@ -893,9 +1006,9 @@ export async function installPlugins(args: InstallPluginsArgs): Promise<() => vo
|
|
|
893
1006
|
throw new PluginSetupError(
|
|
894
1007
|
plugin,
|
|
895
1008
|
// Masked at the source, as plugin-host.ts's `safe()` does (L-11).
|
|
896
|
-
maskedLine(
|
|
897
|
-
|
|
898
|
-
),
|
|
1009
|
+
maskedLine(`plugin '${plugin.name}' failed in setup: ${thrownMessage(err)}`),
|
|
1010
|
+
// The plugin's own code survives, as `safe()` keeps it.
|
|
1011
|
+
errorCode(err) === 'VX_E_INTERNAL' ? 'VX_E_REFUSED' : errorCode(err),
|
|
899
1012
|
)
|
|
900
1013
|
}
|
|
901
1014
|
}
|