mikser-io 9.99.0 → 9.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.js +7 -0
- package/docs/diagnostics.md +11 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/cli.js +139 -0
- package/src/engine.js +43 -1
- package/src/instance.js +17 -0
- package/src/plugins.js +26 -2
- package/src/report.js +50 -0
- package/src/runtime.js +20 -0
package/app.js
CHANGED
|
@@ -68,6 +68,13 @@ function locate(argv) {
|
|
|
68
68
|
// was started without the flag. So the contract has to travel
|
|
69
69
|
// with the request or it is not honoured at all.
|
|
70
70
|
json: has('--json'),
|
|
71
|
+
// Travels for the same reason clear and renderPresets do: it
|
|
72
|
+
// changes what the CYCLE does, and the instance was started
|
|
73
|
+
// without it. A forwarded --force silently did not force — it
|
|
74
|
+
// rebuilt whatever the gates let through, which on a settled tree
|
|
75
|
+
// is nothing, and a caller asking for a full re-render got a no-op
|
|
76
|
+
// reported as success.
|
|
77
|
+
force: has('--force', '-f'),
|
|
71
78
|
renderPresets: has('--render-presets') ? (value('--render-presets') ?? true) : undefined }
|
|
72
79
|
|
|
73
80
|
return {
|
package/docs/diagnostics.md
CHANGED
|
@@ -171,6 +171,17 @@ cycle being reported.
|
|
|
171
171
|
}
|
|
172
172
|
```
|
|
173
173
|
|
|
174
|
+
`plugins` breaks a phase down by who spent it:
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
"plugins": [ { "phase": "finalized", "plugin": "lighthouse", "ms": 6381.1, "calls": 1 } ]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Inside the phases, not beside them — a plugin's time is part of its phase's, so
|
|
181
|
+
the phase totals still add up to `total`. Without it, `finalized` is one number
|
|
182
|
+
covering the reference check, schemas, lint and any audit, and attributing a
|
|
183
|
+
regression to one of them takes a separate measurement each.
|
|
184
|
+
|
|
174
185
|
Ordered slowest first, so a diff between two versions leads with what moved.
|
|
175
186
|
This exists because the report said what was *done* and never what it *took*: a
|
|
176
187
|
preset fan-out that scanned the whole catalog every cycle shipped and ran for
|
package/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export * from './src/auth.js'
|
|
|
5
5
|
export * from './src/roles.js'
|
|
6
6
|
export * from './src/inventory.js'
|
|
7
7
|
export * from './src/report.js'
|
|
8
|
+
export * from './src/cli.js'
|
|
8
9
|
// The diagnostics behind --explain. Exported so a transport — the MCP tool
|
|
9
10
|
// surface, the api plugin's routes — can serve the same structured report the
|
|
10
11
|
// CLI formats, rather than each one reimplementing the question.
|
package/package.json
CHANGED
package/src/cli.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// CLI options a plugin adds.
|
|
2
|
+
//
|
|
3
|
+
// An option is a contract between a person and a build, and a plugin is where
|
|
4
|
+
// half the build lives — so a plugin that cannot add one has to be reached
|
|
5
|
+
// some other way: an environment variable, a config key that means "do the
|
|
6
|
+
// expensive thing", a wrapper script. All three were tried downstream and all
|
|
7
|
+
// three are worse in the same way: they are invisible to `--help`, they cannot
|
|
8
|
+
// be refused when misspelled, and they do not appear beside the checks they
|
|
9
|
+
// belong next to.
|
|
10
|
+
//
|
|
11
|
+
// The reason it was not possible is an ordering fact rather than a decision.
|
|
12
|
+
// The engine parses argv in onInitialize, and the CONFIG — which is where the
|
|
13
|
+
// plugins are named — is not read until onLoad. At the moment the first parse
|
|
14
|
+
// runs, the engine genuinely does not know what options exist.
|
|
15
|
+
//
|
|
16
|
+
// So the parse happens in two stages, and the second one is the real one:
|
|
17
|
+
//
|
|
18
|
+
// 1. onInitialize — core's own options, tolerating unknowns, because the
|
|
19
|
+
// table is knowingly incomplete. Enough to find the config and the
|
|
20
|
+
// folders.
|
|
21
|
+
// 2. after the load phase — every plugin has been constructed and has had
|
|
22
|
+
// its chance to declare, so the table is complete. argv is parsed again
|
|
23
|
+
// against it, and NOW an unknown option is an error.
|
|
24
|
+
//
|
|
25
|
+
// The refusal that has to survive this is the one from 9.81.0: a misspelled
|
|
26
|
+
// flag must never be silently ignored. It is not weakened, only moved to the
|
|
27
|
+
// point where "unknown" can actually be decided.
|
|
28
|
+
|
|
29
|
+
import runtime from './runtime.js'
|
|
30
|
+
|
|
31
|
+
// What plugins declared, for the message when something is not recognised and
|
|
32
|
+
// for anyone asking what a build's option table actually contains.
|
|
33
|
+
const declared = new Map()
|
|
34
|
+
|
|
35
|
+
// Declare a CLI option from inside a plugin.
|
|
36
|
+
//
|
|
37
|
+
// Call it while the plugin is being constructed — that is during the load
|
|
38
|
+
// phase, before the second parse. Declaring later is not an error but the
|
|
39
|
+
// option will not be parsed, so it says so rather than doing nothing.
|
|
40
|
+
//
|
|
41
|
+
// cliOption('--lighthouse', 'audit the built pages with Lighthouse, and exit 0')
|
|
42
|
+
//
|
|
43
|
+
// Reads back from `runtime.options` under commander's usual camel-cased name.
|
|
44
|
+
export function cliOption(flags, description, defaultValue) {
|
|
45
|
+
const commander = runtime.engine?.commander
|
|
46
|
+
if (!commander) {
|
|
47
|
+
throw new Error('cliOption: no commander yet — call it while the plugin is being constructed')
|
|
48
|
+
}
|
|
49
|
+
if (runtime.engine.cliSealed) {
|
|
50
|
+
throw new Error(
|
|
51
|
+
`cliOption(${JSON.stringify(flags)}): the option table was already parsed. `
|
|
52
|
+
+ 'Declare options while the plugin is constructed (the load phase), not afterwards — '
|
|
53
|
+
+ 'a later one would never be read and the flag would look ignored.')
|
|
54
|
+
}
|
|
55
|
+
if (declared.has(flags)) return declared.get(flags)
|
|
56
|
+
const option = defaultValue === undefined
|
|
57
|
+
? commander.option(flags, description)
|
|
58
|
+
: commander.option(flags, description, defaultValue)
|
|
59
|
+
declared.set(flags, option)
|
|
60
|
+
return option
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Every option a plugin added, in declaration order.
|
|
64
|
+
export function pluginCliOptions() {
|
|
65
|
+
return [...declared.keys()]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Stage two: the table is complete, so parse for real.
|
|
69
|
+
//
|
|
70
|
+
// Called once, after the load phase. Merges what the complete table yields
|
|
71
|
+
// over what stage one produced — a plugin's option lands here for the first
|
|
72
|
+
// time, and core's own options parse identically both times.
|
|
73
|
+
export function completeCliParse() {
|
|
74
|
+
const commander = runtime.engine?.commander
|
|
75
|
+
if (!commander || runtime.engine.cliSealed) return
|
|
76
|
+
runtime.engine.cliSealed = true
|
|
77
|
+
|
|
78
|
+
// Always, even when no plugin declared anything.
|
|
79
|
+
//
|
|
80
|
+
// Stage one tolerates an unknown option AND the excess argument it then
|
|
81
|
+
// looks like, so it cannot be the stage that judges argv — left to it,
|
|
82
|
+
// `mikser --bogus-flag` came back "too many arguments. Expected 0
|
|
83
|
+
// arguments but got 1", which is commander describing its own internal
|
|
84
|
+
// reading rather than the mistake a person made. Skipping stage two when
|
|
85
|
+
// no plugin declared would leave exactly that message on most builds.
|
|
86
|
+
//
|
|
87
|
+
// Strict here: every option any part of this build understands is
|
|
88
|
+
// registered, so anything left over is a misspelling, and saying which one
|
|
89
|
+
// is the whole point of the refusal.
|
|
90
|
+
commander.allowUnknownOption(false).allowExcessArguments(false)
|
|
91
|
+
|
|
92
|
+
// Help, now that the table is complete — including every option the
|
|
93
|
+
// project's own plugins declared, which is the whole reason it waited.
|
|
94
|
+
if (runtime.engine.helpRequested) {
|
|
95
|
+
commander.outputHelp()
|
|
96
|
+
process.exit(0)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
commander.parse(process.argv)
|
|
100
|
+
Object.assign(runtime.options, commander.opts())
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The plugin-declared options carried by a client's argv.
|
|
104
|
+
//
|
|
105
|
+
// A forwarded build is answered by an INSTANCE, which parsed its own argv
|
|
106
|
+
// (`--watch`) and never saw the client's. Core's own request options — json,
|
|
107
|
+
// clear, renderPresets — each travel explicitly for exactly this reason. A
|
|
108
|
+
// plugin option has to as well, or `mikser --lighthouse` works with nothing
|
|
109
|
+
// listening and silently does nothing with a watcher up, which is the failure
|
|
110
|
+
// this whole surface exists to remove.
|
|
111
|
+
//
|
|
112
|
+
// Read from the argv the client sent, against the table the INSTANCE has:
|
|
113
|
+
// both processes load the same config, so the instance knows every option the
|
|
114
|
+
// client could legitimately have passed.
|
|
115
|
+
export function pluginOptionsFrom(argv) {
|
|
116
|
+
const commander = runtime.engine?.commander
|
|
117
|
+
if (!commander || !declared.size || !Array.isArray(argv)) return {}
|
|
118
|
+
// A throwaway parse: `parseOptions` reads without applying, so the
|
|
119
|
+
// instance's own options are untouched by asking what a client sent.
|
|
120
|
+
let parsed
|
|
121
|
+
try {
|
|
122
|
+
const probe = commander.createCommand()
|
|
123
|
+
for (const flags of declared.keys()) probe.option(flags, '')
|
|
124
|
+
probe.allowUnknownOption(true).allowExcessArguments(true)
|
|
125
|
+
probe.parse(argv, { from: 'user' })
|
|
126
|
+
parsed = probe.opts()
|
|
127
|
+
} catch {
|
|
128
|
+
return {}
|
|
129
|
+
}
|
|
130
|
+
// Only what a plugin declared. Core's options travel on the request
|
|
131
|
+
// itself, and re-applying them from argv here would give two sources for
|
|
132
|
+
// one answer.
|
|
133
|
+
const names = new Set(Object.keys(parsed))
|
|
134
|
+
const values = {}
|
|
135
|
+
for (const name of names) {
|
|
136
|
+
if (parsed[name] !== undefined) values[name] = parsed[name]
|
|
137
|
+
}
|
|
138
|
+
return values
|
|
139
|
+
}
|
package/src/engine.js
CHANGED
|
@@ -13,6 +13,7 @@ import { OPERATION, TASKS } from './constants.js'
|
|
|
13
13
|
import { changeExtension, formatErrorContext, projectMeta, lookupKeys, siteRootFor } from './utils.js'
|
|
14
14
|
import { reportRendered, reportSkipped, reportError, renderErrorCount, emitReport, finishCycle, reportAssetUse, assetUse } from './report.js'
|
|
15
15
|
import { checkReferences } from './references.js'
|
|
16
|
+
import { completeCliParse } from './cli.js'
|
|
16
17
|
import { fingerprintOutputs } from './fingerprint.js'
|
|
17
18
|
import { toolSchemas, invokeTool, toolResultText, toolResultFailed } from './tools.js'
|
|
18
19
|
import { registerBuiltinTools } from './builtin-tools.js'
|
|
@@ -624,6 +625,11 @@ Which check answers which question:
|
|
|
624
625
|
instance is ALWAYS in watch mode, so watch alone
|
|
625
626
|
answers the wrong question.
|
|
626
627
|
|
|
628
|
+
Is the page fast, and can it be read?
|
|
629
|
+
--lighthouse from mikser-io-lighthouse, if installed. A plugin
|
|
630
|
+
declares its own options now, and they appear here
|
|
631
|
+
beside core's — see cliOption().
|
|
632
|
+
|
|
627
633
|
What did this build do, and cost?
|
|
628
634
|
--json the whole report as one document on stdout, with
|
|
629
635
|
every warning carrying a stable code, and per-phase
|
|
@@ -632,7 +638,34 @@ Which check answers which question:
|
|
|
632
638
|
|
|
633
639
|
The full version, with what each code means: docs/diagnostics.md`)
|
|
634
640
|
|
|
635
|
-
|
|
641
|
+
// Stage one of two. The config names the plugins and is not read
|
|
642
|
+
// until onLoad, so at this moment the engine does not yet know every
|
|
643
|
+
// option this build understands — a plugin's is not registered. Left
|
|
644
|
+
// strict, `mikser --lighthouse` would be rejected before the plugin
|
|
645
|
+
// that defines it had been constructed.
|
|
646
|
+
//
|
|
647
|
+
// Tolerated here and decided in stage two, where the table is complete
|
|
648
|
+
// — see completeCliParse(). The 9.81.0 refusal is not weakened, it is
|
|
649
|
+
// moved to the point where "unknown" can be answered.
|
|
650
|
+
runtime.engine.commander.allowUnknownOption(true).allowExcessArguments(true)
|
|
651
|
+
|
|
652
|
+
// --help is answered in stage two, for the same reason the parse is.
|
|
653
|
+
//
|
|
654
|
+
// Commander prints help and exits the moment it sees the flag, which
|
|
655
|
+
// in stage one is before any plugin has been constructed — so the help
|
|
656
|
+
// would list core's options and silently omit every option the
|
|
657
|
+
// project's own plugins add. A help text that is missing the flag you
|
|
658
|
+
// are looking for is worse than a slower one.
|
|
659
|
+
//
|
|
660
|
+
// Held out of this parse and answered after the table is complete. It
|
|
661
|
+
// does mean `--help` now loads the config, which is the honest cost of
|
|
662
|
+
// describing THIS project's mikser rather than a generic one.
|
|
663
|
+
const helpFlags = ['--help', '-h']
|
|
664
|
+
runtime.engine.helpRequested = process.argv.some(arg => helpFlags.includes(arg))
|
|
665
|
+
const argv = runtime.engine.helpRequested
|
|
666
|
+
? process.argv.filter(arg => !helpFlags.includes(arg))
|
|
667
|
+
: process.argv
|
|
668
|
+
Object.assign(runtime.options, options || runtime.engine.commander.parse(argv).opts())
|
|
636
669
|
// runtime.options.info gates the progress bar — gauge stays
|
|
637
670
|
// silent in --debug/--trace modes because logs are voluminous
|
|
638
671
|
// there and a bar on top would just be noise.
|
|
@@ -778,6 +811,15 @@ The full version, with what each code means: docs/diagnostics.md`)
|
|
|
778
811
|
})
|
|
779
812
|
|
|
780
813
|
onLoaded(async () => {
|
|
814
|
+
// Stage two of the parse, before anything reads an option.
|
|
815
|
+
//
|
|
816
|
+
// Every plugin has been constructed by now and has had its chance to
|
|
817
|
+
// declare, so the table is complete and argv is parsed against all of
|
|
818
|
+
// it. This engine hook is registered when the module is imported, so
|
|
819
|
+
// it runs ahead of every plugin's own onLoaded — which is what makes
|
|
820
|
+
// it safe for a plugin to read its own option there.
|
|
821
|
+
completeCliParse()
|
|
822
|
+
|
|
781
823
|
const logger = useLogger()
|
|
782
824
|
logger.debug(runtime.options, 'Mikser options')
|
|
783
825
|
|
package/src/instance.js
CHANGED
|
@@ -38,6 +38,7 @@ import { onLoaded } from './lifecycle.js'
|
|
|
38
38
|
import { renderErrorCount } from './report.js'
|
|
39
39
|
import { runReportOnly } from './engine.js'
|
|
40
40
|
import { emitReport } from './report.js'
|
|
41
|
+
import { pluginOptionsFrom } from './cli.js'
|
|
41
42
|
|
|
42
43
|
// Where the endpoint lives.
|
|
43
44
|
//
|
|
@@ -416,6 +417,17 @@ async function withRequestOutput(request, run) {
|
|
|
416
417
|
prior[key] = runtime.options[key]
|
|
417
418
|
if (request[key]) runtime.options[key] = request[key]
|
|
418
419
|
}
|
|
420
|
+
|
|
421
|
+
// Whatever the client's argv said about a PLUGIN's options.
|
|
422
|
+
//
|
|
423
|
+
// The instance parsed its own argv and never saw the client's, so a flag a
|
|
424
|
+
// plugin declared worked with nothing listening and did nothing with a
|
|
425
|
+
// watcher up. Applied and restored with the rest of the request's
|
|
426
|
+
// contract, so a watcher's own cycle before or after is unaffected.
|
|
427
|
+
for (const [name, value] of Object.entries(pluginOptionsFrom(request.argv))) {
|
|
428
|
+
if (!(name in prior)) prior[name] = runtime.options[name]
|
|
429
|
+
runtime.options[name] = value
|
|
430
|
+
}
|
|
419
431
|
try {
|
|
420
432
|
return await run()
|
|
421
433
|
} finally {
|
|
@@ -450,6 +462,10 @@ async function serveBuild(socket, request, logger) {
|
|
|
450
462
|
// rebuild.
|
|
451
463
|
const priorRenderPresets = runtime.options.renderPresets
|
|
452
464
|
if (request.renderPresets !== undefined) runtime.options.renderPresets = request.renderPresets
|
|
465
|
+
// Applied for THIS cycle only, like renderPresets. An instance left in
|
|
466
|
+
// force mode would re-render the whole site on every later save.
|
|
467
|
+
const priorForce = runtime.options.force
|
|
468
|
+
if (request.force) runtime.options.force = true
|
|
453
469
|
try {
|
|
454
470
|
// The report is emitted by the cycle itself, from inside
|
|
455
471
|
// rebuild() — the same call a one-shot makes. Nothing here
|
|
@@ -471,6 +487,7 @@ async function serveBuild(socket, request, logger) {
|
|
|
471
487
|
})
|
|
472
488
|
} finally {
|
|
473
489
|
runtime.options.renderPresets = priorRenderPresets
|
|
490
|
+
runtime.options.force = priorForce
|
|
474
491
|
}
|
|
475
492
|
|
|
476
493
|
// From the render-error count, NOT from process.exitCode.
|
package/src/plugins.js
CHANGED
|
@@ -110,11 +110,35 @@ onLoad(() => {
|
|
|
110
110
|
if (registeredPostprocessors) parts.push(`${registeredPostprocessors} postprocessor${registeredPostprocessors === 1 ? '' : 's'}`)
|
|
111
111
|
logger.info('Loading plugins: %s', parts.join(', '))
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
// Which plugin registered which hook, so a phase can be broken down.
|
|
114
|
+
//
|
|
115
|
+
// `timings` said `finalized: 19400ms` and stopped there — and finalized is
|
|
116
|
+
// where the reference check, schemas, lint and lighthouse all live, so the
|
|
117
|
+
// one number said nothing about which of them cost it. Downstream that
|
|
118
|
+
// took three separate measurements to attribute a 465ms lint pass.
|
|
119
|
+
//
|
|
120
|
+
// Labelled retroactively, because a plugin's identity is not knowable
|
|
121
|
+
// until its factory RETURNS: the entry in the plugins array is already the
|
|
122
|
+
// factory's result, so there is no name to read going in, and the
|
|
123
|
+
// descriptor with `collection` on it only exists coming out. So the hooks
|
|
124
|
+
// are diffed across the call and tagged with what the call produced.
|
|
125
|
+
const hookNames = Object.keys(runtime.hooks)
|
|
126
|
+
for (const [index, factoryReturn] of factoryEntries.entries()) {
|
|
127
|
+
const before = new Map(hookNames.map(name => [name, runtime.hooks[name].length]))
|
|
128
|
+
let descriptor
|
|
114
129
|
try {
|
|
115
|
-
factoryReturn(core)
|
|
130
|
+
descriptor = factoryReturn(core)
|
|
116
131
|
} catch (err) {
|
|
117
132
|
logger.error('Plugin factory threw on registration: %s', err.message)
|
|
133
|
+
continue
|
|
134
|
+
}
|
|
135
|
+
const label = descriptor?.collection ?? descriptor?.type ?? `plugin-${index + 1}`
|
|
136
|
+
for (const name of hookNames) {
|
|
137
|
+
for (const hook of runtime.hooks[name].slice(before.get(name))) {
|
|
138
|
+
// A plugin registering the same function twice keeps its first
|
|
139
|
+
// label rather than being renamed by a later registration.
|
|
140
|
+
if (typeof hook === 'function' && !hook.mikserPlugin) hook.mikserPlugin = label
|
|
141
|
+
}
|
|
118
142
|
}
|
|
119
143
|
}
|
|
120
144
|
})
|
package/src/report.js
CHANGED
|
@@ -126,6 +126,8 @@ export function resetReport() {
|
|
|
126
126
|
// asking. The boot phases therefore appear in the first cycle's report and
|
|
127
127
|
// not in later ones, which is what actually happened.
|
|
128
128
|
runtime.state.timings = {}
|
|
129
|
+
runtime.state.pluginTimings = {}
|
|
130
|
+
runtime.state.activity = { rendered: 0, changed: 0 }
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
// Published on the runtime so runtime.js can start a fresh cycle for a
|
|
@@ -194,6 +196,7 @@ export function reportWipe(cause, detail = {}) {
|
|
|
194
196
|
// One source whose bytes moved. The complement of reportGated: between them
|
|
195
197
|
// every file the engine looked at is accounted for.
|
|
196
198
|
export function reportChanged(id) {
|
|
199
|
+
if (id) activity().changed++
|
|
197
200
|
if (!reportWanted() || !id) return
|
|
198
201
|
const store = changedStore()
|
|
199
202
|
store.count++
|
|
@@ -277,6 +280,7 @@ export function reportGated(count = 1) {
|
|
|
277
280
|
// `matched`, `dependency` each mean something specific, and a single
|
|
278
281
|
// polymorphic key would push the type switch onto every consumer.
|
|
279
282
|
export function reportRendered(entity, reason, decision = {}) {
|
|
283
|
+
activity().rendered++
|
|
280
284
|
if (!reportWanted()) return
|
|
281
285
|
store().rendered.push({
|
|
282
286
|
id: entity?.id,
|
|
@@ -425,6 +429,45 @@ export function renderErrorCount() {
|
|
|
425
429
|
return errorStore().length
|
|
426
430
|
}
|
|
427
431
|
|
|
432
|
+
// A bare count of what the cycle did, kept whether or not anyone is reading
|
|
433
|
+
// the report.
|
|
434
|
+
//
|
|
435
|
+
// The rendered/skipped/unchanged ARRAYS are only recorded when something can
|
|
436
|
+
// read them — one entry per entity per cycle is not free, which is why `gated`
|
|
437
|
+
// was already a bare count. So a check asking "did this cycle do anything"
|
|
438
|
+
// cannot read those arrays: without --json they are empty, and a cold build
|
|
439
|
+
// that rendered the whole site looks identical to a no-op. That is exactly the
|
|
440
|
+
// mistake the first version of cycleMovedNothing() made, and it reported
|
|
441
|
+
// "nothing moved" on a build that had just rendered.
|
|
442
|
+
//
|
|
443
|
+
// Two integers cost nothing and are always true.
|
|
444
|
+
function activity() {
|
|
445
|
+
runtime.state ??= {}
|
|
446
|
+
runtime.state.activity ??= { rendered: 0, changed: 0 }
|
|
447
|
+
return runtime.state.activity
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Did this cycle move anything?
|
|
451
|
+
//
|
|
452
|
+
// For a check that reads the OUTPUT — a linter, an audit — a cycle that
|
|
453
|
+
// rendered nothing and changed nothing produced the same bytes the last one
|
|
454
|
+
// did, so running again spends time to reprint what was already said.
|
|
455
|
+
// Downstream that was measured: a lint pass costing 465ms of every no-op
|
|
456
|
+
// rebuild, and a Lighthouse audit costing 19 SECONDS of one.
|
|
457
|
+
//
|
|
458
|
+
// Both halves matter. `rendered` alone is not enough: files() emits by
|
|
459
|
+
// symlinking rather than rendering, so a new static file changes the output
|
|
460
|
+
// with nothing rendered. `changed` covers that — it counts the sources whose
|
|
461
|
+
// bytes moved this cycle.
|
|
462
|
+
//
|
|
463
|
+
// A plugin skipping on this must SAY so. "Nothing to report" and "did not run"
|
|
464
|
+
// are indistinguishable otherwise, which is the property this codebase keeps
|
|
465
|
+
// having to remove.
|
|
466
|
+
export function cycleMovedNothing() {
|
|
467
|
+
const { rendered, changed } = activity()
|
|
468
|
+
return rendered === 0 && changed === 0
|
|
469
|
+
}
|
|
470
|
+
|
|
428
471
|
// The cause, and enough detail to act on it.
|
|
429
472
|
//
|
|
430
473
|
// A wipe outranks changed sources: when the cache went, everything is a
|
|
@@ -451,6 +494,12 @@ function phaseTimings() {
|
|
|
451
494
|
const entries = Object.entries(timings)
|
|
452
495
|
.map(([phase, { ms, calls }]) => ({ phase, ms: Math.round(ms * 10) / 10, calls }))
|
|
453
496
|
.sort((a, b) => b.ms - a.ms)
|
|
497
|
+
// Inside the phases, not beside them: a plugin's time is part of its
|
|
498
|
+
// phase's, so this is a breakdown rather than an addition.
|
|
499
|
+
const byPlugin = Object.values(runtime.state?.pluginTimings ?? {})
|
|
500
|
+
.map(({ phase, plugin, ms, calls }) => ({ phase, plugin, ms: Math.round(ms * 10) / 10, calls }))
|
|
501
|
+
.sort((a, b) => b.ms - a.ms)
|
|
502
|
+
|
|
454
503
|
return {
|
|
455
504
|
// This cycle, summed. The phases below add up to it.
|
|
456
505
|
total: Math.round(entries.reduce((sum, e) => sum + e.ms, 0) * 10) / 10,
|
|
@@ -467,6 +516,7 @@ function phaseTimings() {
|
|
|
467
516
|
// misled by a name like "elapsed".
|
|
468
517
|
processUptime: Math.round(process.uptime() * 1000 * 10) / 10,
|
|
469
518
|
phases: entries,
|
|
519
|
+
...(byPlugin.length ? { plugins: byPlugin } : {}),
|
|
470
520
|
}
|
|
471
521
|
}
|
|
472
522
|
|
package/src/runtime.js
CHANGED
|
@@ -82,6 +82,19 @@ const runtime = {
|
|
|
82
82
|
//
|
|
83
83
|
// Accumulated per phase rather than assigned, because a phase runs more
|
|
84
84
|
// than once in a watch process and a cycle can re-enter one.
|
|
85
|
+
// Per plugin within a phase. Kept apart from `phases` so the phase totals
|
|
86
|
+
// still add up to `total` — a plugin's time is INSIDE its phase, not
|
|
87
|
+
// beside it, and adding both would double-count the run.
|
|
88
|
+
recordPluginPhase(phaseName, plugin, ms) {
|
|
89
|
+
if (!phaseName || !plugin) return
|
|
90
|
+
this.state ??= {}
|
|
91
|
+
const byPlugin = (this.state.pluginTimings ??= {})
|
|
92
|
+
const key = `${phaseName}:${plugin}`
|
|
93
|
+
const entry = (byPlugin[key] ??= { phase: phaseName, plugin, ms: 0, calls: 0 })
|
|
94
|
+
entry.ms += ms
|
|
95
|
+
entry.calls++
|
|
96
|
+
},
|
|
97
|
+
|
|
85
98
|
recordPhase(phaseName, ms) {
|
|
86
99
|
if (!phaseName) return
|
|
87
100
|
this.state ??= {}
|
|
@@ -101,7 +114,14 @@ const runtime = {
|
|
|
101
114
|
try {
|
|
102
115
|
for (let hook of hooks) {
|
|
103
116
|
if (signal?.aborted) throw new AbortError()
|
|
117
|
+
// Timed per hook when it belongs to a plugin, so a phase can
|
|
118
|
+
// be broken down by who spent it. `finalized` alone is where
|
|
119
|
+
// the reference check, schemas, lint and an audit all live.
|
|
120
|
+
const hookStarted = hook.mikserPlugin ? performance.now() : 0
|
|
104
121
|
await hook(signal)
|
|
122
|
+
if (hook.mikserPlugin) {
|
|
123
|
+
this.recordPluginPhase(phaseName, hook.mikserPlugin, performance.now() - hookStarted)
|
|
124
|
+
}
|
|
105
125
|
}
|
|
106
126
|
} finally {
|
|
107
127
|
// In `finally`, so a phase that threw still reports what it spent
|