mikser-io 10.14.0 → 11.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "10.14.0",
3
+ "version": "11.0.2",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
package/src/inventory.js CHANGED
@@ -30,39 +30,29 @@ function repositoryUrl(repository) {
30
30
  .replace(/\.git$/, '')
31
31
  }
32
32
 
33
- // Which plugins are actually RUNNING, as opposed to merely installed.
33
+ // Every mikser package INSTALLED beside this one, described.
34
34
  //
35
- // The distinction is the useful half: a package in node_modules that no config
36
- // loads explains nothing about the site's behaviour, and an agent told
37
- // otherwise will look for a feature that is not switched on.
38
- function activeNames() {
39
- const active = new Set()
40
- const named = (short) => (short.startsWith('mikser-io') ? short : `mikser-io-${short}`)
41
-
42
- // Every plugin that mounts a route already names itself there the
43
- // strongest signal available, and one that stays correct as plugins are
44
- // added, because registerRoute requires it.
45
- for (const route of runtime.routes ?? []) {
46
- if (route?.plugin) active.add(named(route.plugin))
47
- }
48
- // Renderers and postprocessors are registered by name rather than by
49
- // package, and the package name is that name in a fixed shape.
50
- for (const name of runtime.renderers?.keys() ?? []) active.add(`mikser-io-render-${name}`)
51
- for (const name of runtime.postprocessors?.keys() ?? []) active.add(`mikser-io-post-${name}`)
52
- // A lifecycle plugin that mounts nothing is recognised by the surface it
53
- // publishes on the runtime.
54
- if (runtime.options?.layouts) active.add('mikser-io-layouts')
55
- if (runtime.options?.preview) active.add('mikser-io-preview')
56
- // The engine itself is always running; saying otherwise would be odd.
57
- active.add('mikser-io')
58
- return active
59
- }
60
-
61
- // Every mikser package installed beside this one, described.
35
+ // Installed, and nothing more. This list used to carry an `active` flag,
36
+ // derived by probing whatever surfaces a plugin happened to expose — a route
37
+ // here, a CLI flag there. Two things were wrong with it and only the second
38
+ // was dangerous.
39
+ //
40
+ // The probe went stale silently: it tested `runtime.options.layouts`, which
41
+ // stopped being layouts' API object two majors ago and is now the `--layouts`
42
+ // folder flag, so layouts read as inactive on every site that did not pass a
43
+ // flag it has no reason to pass. It also tested `runtime.options.preview` for
44
+ // a package, `mikser-io-preview`, that does not exist.
45
+ //
46
+ // And the flag was three-valued in code and two-valued in its contract:
47
+ // present meant running, absent meant EITHER not running or not detectable,
48
+ // and an agent told to read it "to know what the system can do" could only
49
+ // read absence as off. On a real site that said no schema validation and no
50
+ // git sync while both were running — and git sync is the only route by which
51
+ // an agent's own edit reaches the repository.
62
52
  //
63
- // `active` is reported only where it can be established: a lifecycle plugin
64
- // that publishes nothing on the runtime cannot be detected, and saying `false`
65
- // for it would be a claim rather than an absence of one.
53
+ // What is running is now answered by the runtime recording what it loads, in
54
+ // plugins.js. This answers a different and still useful question: what is on
55
+ // disk, what version, and where to read about it.
66
56
  export function inventory({ workingFolder = runtime.options?.workingFolder } = {}) {
67
57
  const root = path.join(workingFolder ?? '.', 'node_modules')
68
58
  let names = []
@@ -72,7 +62,6 @@ export function inventory({ workingFolder = runtime.options?.workingFolder } = {
72
62
  return []
73
63
  }
74
64
 
75
- const active = activeNames()
76
65
  const plugins = []
77
66
  for (const name of names.sort()) {
78
67
  try {
@@ -82,7 +71,6 @@ export function inventory({ workingFolder = runtime.options?.workingFolder } = {
82
71
  name,
83
72
  version: manifest.version ?? null,
84
73
  ...(manifest.description ? { summary: manifest.description } : {}),
85
- ...(active.has(name) ? { active: true } : {}),
86
74
  ...(manifest.homepage ? { homepage: manifest.homepage } : {}),
87
75
  ...(repository ? { repository } : {}),
88
76
  npm: `https://www.npmjs.com/package/${name}`,
@@ -91,3 +79,25 @@ export function inventory({ workingFolder = runtime.options?.workingFolder } = {
91
79
  }
92
80
  return plugins
93
81
  }
82
+
83
+ // What this runtime LOADED — the answer to "what is running".
84
+ //
85
+ // Read from the record plugins.js keeps as it loads, not derived from
86
+ // surfaces afterwards. Every loaded plugin appears, including one that named
87
+ // nothing: `package: null` means "running, and did not say what it is", which
88
+ // is a different statement from not running and must never be collapsed into
89
+ // one. Nothing here is ever absent because it could not be detected — a plugin
90
+ // that is loaded is in this list.
91
+ export function loadedPlugins({ workingFolder = runtime.options?.workingFolder } = {}) {
92
+ const root = path.join(workingFolder ?? '.', 'node_modules')
93
+ const versionOf = (name) => {
94
+ if (!name) return null
95
+ try {
96
+ return JSON.parse(readFileSync(path.join(root, name, 'package.json'), 'utf8')).version ?? null
97
+ } catch { return null }
98
+ }
99
+ return (runtime.plugins ?? []).map(entry => ({
100
+ ...entry,
101
+ ...(entry.package ? { version: versionOf(entry.package) } : {}),
102
+ }))
103
+ }
@@ -1209,5 +1209,10 @@ export function api(options = {}) {
1209
1209
  }
1210
1210
  }
1211
1211
  })
1212
+
1213
+ // Names this package to the runtime's loaded-plugin record — see
1214
+ // plugins.js. A plugin that declares nothing still reports as loaded,
1215
+ // but as `package: null`.
1216
+ return { module: import.meta.url }
1212
1217
  }
1213
1218
  }
@@ -304,6 +304,6 @@ export function commands(options = {}) {
304
304
  reportUnfired()
305
305
  })
306
306
 
307
- return { executeCommand }
307
+ return { executeCommand, module: import.meta.url }
308
308
  }
309
309
  }
@@ -216,5 +216,10 @@ export function data(options = {}) {
216
216
  }
217
217
  }
218
218
  })
219
+
220
+ // Names this package to the runtime's loaded-plugin record — see
221
+ // plugins.js. A plugin that declares nothing still reports as loaded,
222
+ // but as `package: null`.
223
+ return { module: import.meta.url }
219
224
  }
220
225
  }
@@ -29,6 +29,6 @@ export function documents(options = {}) {
29
29
  phase: 'import',
30
30
  })
31
31
 
32
- return { collection, type }
32
+ return { collection, type, module: import.meta.url }
33
33
  }
34
34
  }
@@ -273,6 +273,7 @@ export function files(options = {}) {
273
273
  return {
274
274
  collection,
275
275
  type,
276
+ module: import.meta.url,
276
277
  }
277
278
  }
278
279
  }
@@ -20,5 +20,10 @@ export function frontMatter(options = {}) {
20
20
  }
21
21
  }
22
22
  })
23
+
24
+ // Names this package to the runtime's loaded-plugin record — see
25
+ // plugins.js. A plugin that declares nothing still reports as loaded,
26
+ // but as `package: null`.
27
+ return { module: import.meta.url }
23
28
  }
24
29
  }
@@ -16,5 +16,10 @@ export function json(options = {}) {
16
16
  }
17
17
  }
18
18
  })
19
+
20
+ // Names this package to the runtime's loaded-plugin record — see
21
+ // plugins.js. A plugin that declares nothing still reports as loaded,
22
+ // but as `package: null`.
23
+ return { module: import.meta.url }
19
24
  }
20
25
  }
@@ -22,5 +22,10 @@ export function mapper(options = {}) {
22
22
  }
23
23
  }
24
24
  })
25
+
26
+ // Names this package to the runtime's loaded-plugin record — see
27
+ // plugins.js. A plugin that declares nothing still reports as loaded,
28
+ // but as `package: null`.
29
+ return { module: import.meta.url }
25
30
  }
26
31
  }
@@ -212,6 +212,7 @@ export function observer(options = {}) {
212
212
 
213
213
  return {
214
214
  format,
215
+ module: import.meta.url,
215
216
  }
216
217
  }
217
218
  }
@@ -183,6 +183,6 @@ export function preview(options = {}) {
183
183
  })
184
184
  })
185
185
 
186
- return { name: 'preview' }
186
+ return { name: 'preview', module: import.meta.url }
187
187
  }
188
188
  }
@@ -164,5 +164,5 @@ export function load({ runtime, options, track, logger }) {
164
164
  }
165
165
 
166
166
  export function fileHelpers(options = {}) {
167
- return { name: options.name ?? 'file', options, load }
167
+ return { name: options.name ?? 'file', options, load, module: import.meta.url }
168
168
  }
@@ -327,5 +327,6 @@ export function renderHbs(options = {}) {
327
327
  load,
328
328
  render,
329
329
  parseReferences,
330
+ module: import.meta.url,
330
331
  }
331
332
  }
@@ -41,5 +41,5 @@ export function load({ entity, runtime, options }) {
41
41
  }
42
42
 
43
43
  export function hrefUrlHelpers(options = {}) {
44
- return { name: options.name ?? 'href', options, load }
44
+ return { name: options.name ?? 'href', options, load, module: import.meta.url }
45
45
  }
@@ -24,5 +24,5 @@ export function load({ runtime, entity, state, options, track }) {
24
24
  }
25
25
 
26
26
  export function resourceUrlHelper(options = {}) {
27
- return { name: options.name ?? 'resource', options, load }
27
+ return { name: options.name ?? 'resource', options, load, module: import.meta.url }
28
28
  }
@@ -245,6 +245,7 @@ export function resources(options = {}) {
245
245
  return {
246
246
  collection,
247
247
  type,
248
+ module: import.meta.url,
248
249
  }
249
250
  }
250
251
  }
@@ -38,5 +38,10 @@ export function shares(options = {}) {
38
38
  }
39
39
  }
40
40
  })
41
+
42
+ // Names this package to the runtime's loaded-plugin record — see
43
+ // plugins.js. A plugin that declares nothing still reports as loaded,
44
+ // but as `package: null`.
45
+ return { module: import.meta.url }
41
46
  }
42
47
  }
@@ -31,8 +31,10 @@ export function sources(options = {}) {
31
31
  const { useLogger } = core
32
32
  if (!collections.length) {
33
33
  // No collections is a legitimate config (a flag turned them all
34
- // off); nothing to register, and nothing to complain about.
35
- return
34
+ // off); nothing to register, and nothing to complain about. Still
35
+ // names itself: the plugin IS loaded, and a config that turned its
36
+ // collections off must not make it look undetectable.
37
+ return { module: import.meta.url }
36
38
  }
37
39
  for (const [collection, config] of collections) {
38
40
  const {
@@ -63,6 +65,9 @@ export function sources(options = {}) {
63
65
  })
64
66
  }
65
67
  useLogger?.()?.debug('Sources registered: %s', collections.map(([c]) => c).join(', '))
68
+ // Names this package to the runtime's loaded-plugin record, so ping
69
+ // reports it as running rather than as undetectable.
70
+ return { module: import.meta.url }
66
71
  }
67
72
  }
68
73
 
@@ -14,5 +14,10 @@ export function validator(options = {}) {
14
14
  })
15
15
  }
16
16
  })
17
+
18
+ // Names this package to the runtime's loaded-plugin record — see
19
+ // plugins.js. A plugin that declares nothing still reports as loaded,
20
+ // but as `package: null`.
21
+ return { module: import.meta.url }
17
22
  }
18
23
  }
@@ -22,5 +22,10 @@ export function yaml(options = {}) {
22
22
  }
23
23
  }
24
24
  })
25
+
26
+ // Names this package to the runtime's loaded-plugin record — see
27
+ // plugins.js. A plugin that declares nothing still reports as loaded,
28
+ // but as `package: null`.
29
+ return { module: import.meta.url }
25
30
  }
26
31
  }
package/src/plugins.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { useLogger } from './engine/index.js'
2
2
  import { onLoad } from './lifecycle.js'
3
3
  import { resetServices } from './services.js'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+ import { existsSync, readFileSync } from 'node:fs'
4
7
  import runtime from './runtime.js'
5
8
 
6
9
  import * as core from '../index.js'
@@ -40,6 +43,9 @@ onLoad(() => {
40
43
  // always sees a Map, never undefined.
41
44
  runtime.renderers = runtime.renderers ?? new Map()
42
45
  runtime.postprocessors = runtime.postprocessors ?? new Map()
46
+ // Rebuilt, not appended to: loadPlugins runs again on a config change, and
47
+ // a list that accumulated would report every plugin twice.
48
+ runtime.plugins = []
43
49
 
44
50
  const factoryEntries = []
45
51
  let registeredRenderers = 0
@@ -62,6 +68,8 @@ onLoad(() => {
62
68
  continue
63
69
  }
64
70
  runtime.renderers.set(name, entry)
71
+ recordPlugin({ kind: 'renderer', name,
72
+ package: packageOfModule(entry.module), module: entry.module ?? null })
65
73
  registeredRenderers++
66
74
  continue
67
75
  }
@@ -72,6 +80,8 @@ onLoad(() => {
72
80
  continue
73
81
  }
74
82
  runtime.postprocessors.set(name, entry)
83
+ recordPlugin({ kind: 'postprocessor', name,
84
+ package: packageOfModule(entry.module), module: entry.module ?? null })
75
85
  registeredPostprocessors++
76
86
  continue
77
87
  }
@@ -140,17 +150,83 @@ onLoad(() => {
140
150
  logger.error('Plugin factory threw on registration: %s', err.message)
141
151
  continue
142
152
  }
143
- const label = descriptor?.collection ?? descriptor?.type ?? `plugin-${index + 1}`
153
+ // `plugin-7` names nothing a reader can act on. A plugin that
154
+ // declared its module has already said where it is, so the file's own
155
+ // stem is a better name than its position in an array.
156
+ const declaredName = descriptor?.module
157
+ ? path.basename(String(descriptor.module).split('?')[0]).replace(/\.[cm]?js$/, '')
158
+ : null
159
+ const label = descriptor?.collection ?? descriptor?.type ?? declaredName ?? `plugin-${index + 1}`
160
+ const registered = []
144
161
  for (const name of hookNames) {
145
- for (const hook of runtime.hooks[name].slice(before.get(name))) {
162
+ const added = runtime.hooks[name].slice(before.get(name))
163
+ if (added.length) registered.push(name)
164
+ for (const hook of added) {
146
165
  // A plugin registering the same function twice keeps its first
147
166
  // label rather than being renamed by a later registration.
148
167
  if (typeof hook === 'function' && !hook.mikserPlugin) hook.mikserPlugin = label
149
168
  }
150
169
  }
170
+ // Recorded whether or not it named itself. A plugin that declares
171
+ // nothing is still loaded, and the list must say so — that is the
172
+ // whole difference between this and the probe it replaces.
173
+ recordPlugin({
174
+ kind: 'lifecycle',
175
+ label,
176
+ package: packageOfModule(descriptor?.module),
177
+ module: descriptor?.module ?? null,
178
+ ...(descriptor?.collection ? { collection: descriptor.collection } : {}),
179
+ hooks: registered,
180
+ })
151
181
  }
152
182
  })
153
183
 
184
+
185
+ // The package a module belongs to, read from the nearest package.json.
186
+ //
187
+ // Walked up from the file rather than pattern-matched on the path: a plugin
188
+ // developed in a workspace sits at `~/Projects/mikser/mikser-io-git`, and the
189
+ // same plugin installed sits at `<site>/node_modules/mikser-io-git`. Only the
190
+ // manifest is true in both, and only the manifest is true for a plugin whose
191
+ // package is not named for it.
192
+ function packageOfModule(moduleUrl) {
193
+ if (typeof moduleUrl !== 'string') return null
194
+ let dir
195
+ try {
196
+ dir = path.dirname(moduleUrl.startsWith('file:') ? fileURLToPath(moduleUrl) : moduleUrl)
197
+ } catch { return null }
198
+ for (let depth = 0; depth < 12; depth++) {
199
+ const manifest = path.join(dir, 'package.json')
200
+ if (existsSync(manifest)) {
201
+ try { return JSON.parse(readFileSync(manifest, 'utf8')).name ?? null } catch { return null }
202
+ }
203
+ const parent = path.dirname(dir)
204
+ if (parent === dir) break
205
+ dir = parent
206
+ }
207
+ return null
208
+ }
209
+
210
+ // What this runtime LOADED, recorded as it loads it.
211
+ //
212
+ // Not derived afterwards. The previous answer was assembled by inventory.js
213
+ // from whatever surfaces happened to be observable — a route here, a CLI flag
214
+ // there — which made "is this plugin running" a question about how well the
215
+ // probe was maintained rather than about the plugin. It reported layouts as
216
+ // not running on every site that did not pass `--layouts`, because the flag it
217
+ // probed had stopped being an API object two majors earlier and nothing failed
218
+ // when it went stale.
219
+ //
220
+ // A plugin names itself by returning `module: import.meta.url` from its
221
+ // factory, the same self-naming `registerRoute` and `provideService` already
222
+ // require. One that names nothing is still recorded and still reported as
223
+ // LOADED — its `package` is null, which says "this is running and did not say
224
+ // what it is", never "this is not running".
225
+ function recordPlugin(entry) {
226
+ runtime.plugins = runtime.plugins ?? []
227
+ runtime.plugins.push(entry)
228
+ }
229
+
154
230
  function kebabToCamel(s) {
155
231
  return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
156
232
  }