mikser-io-layouts 3.1.0 → 11.1.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.
@@ -0,0 +1,73 @@
1
+ # Publish to npm without a stored token.
2
+ #
3
+ # npm is retiring tokens that skip the second factor — its own token page says
4
+ # "Publish new versions directly (deprecated — ends January 2027)". This is the
5
+ # replacement: the job proves who it is at publish time with a short-lived
6
+ # credential GitHub issues and npm verifies, so npm trusts "the publish.yml
7
+ # workflow in this repository" rather than a string.
8
+ #
9
+ # There is no NODE_AUTH_TOKEN and no secret to configure. Nothing to leak,
10
+ # expire or rotate — the two ways publishing broke in September 2026.
11
+ #
12
+ # Link it once on npm: the package → Settings → Trusted Publisher → GitHub
13
+ # Actions, naming this repository and this filename. Until that exists the
14
+ # publish step fails rather than falling back to something weaker.
15
+
16
+ name: Publish
17
+
18
+ on:
19
+ push:
20
+ tags: ['v*']
21
+ workflow_dispatch:
22
+
23
+ jobs:
24
+ publish:
25
+ runs-on: ubuntu-latest
26
+ permissions:
27
+ contents: read
28
+ # The whole mechanism. Without it npm has nothing to verify.
29
+ id-token: write
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+
33
+ - uses: actions/setup-node@v4
34
+ with:
35
+ node-version: '24'
36
+ registry-url: 'https://registry.npmjs.org'
37
+
38
+ # Trusted publishing landed in npm 11.5.1.
39
+ - name: Use an npm that can do trusted publishing
40
+ run: npm install -g npm@latest
41
+
42
+ # `npm install`, not `npm ci`: these packages are developed in a
43
+ # workspace, so dependencies hoist to its root and each package's own
44
+ # lockfile is never read locally. They drift silently, and CI is the only
45
+ # thing that would ever read them.
46
+ - run: npm install --no-audit --no-fund
47
+ env:
48
+ # puppeteer downloads a browser on install; nothing in the release
49
+ # path renders a page.
50
+ PUPPETEER_SKIP_DOWNLOAD: '1'
51
+
52
+ # The tag is the release. A tag that disagrees with package.json would
53
+ # publish a version nobody asked for.
54
+ - name: Tag must match package.json
55
+ if: startsWith(github.ref, 'refs/tags/v')
56
+ run: |
57
+ TAG="${GITHUB_REF_NAME#v}"
58
+ PKG="$(node -p 'require("./package.json").version')"
59
+ if [ "$TAG" != "$PKG" ]; then
60
+ echo "tag v$TAG does not match package.json $PKG"
61
+ exit 1
62
+ fi
63
+ echo "publishing $PKG"
64
+
65
+ # The suite runs here now. It did not before: these packages declared
66
+ # mikser-io only as a peerDependency (npm does not install a package's
67
+ # own peers) or as file:../mikser-io (a path that exists on a laptop and
68
+ # nowhere else), so a standalone clone could not resolve the engine it
69
+ # tests against. Both are now real devDependencies with version ranges,
70
+ # which npm still satisfies from the workspace locally.
71
+ - run: npm test
72
+
73
+ - run: npm publish --access public
package/index.js CHANGED
@@ -19,7 +19,7 @@ import _ from 'lodash'
19
19
  import {
20
20
  gateChecksum, sweepDeleted, scanSummary,
21
21
  checksumsByCollection,
22
- checksum as fileChecksum, checksumOf,
22
+ checksum as fileChecksum, checksumOf, registerSourceChecksum,
23
23
  useDatabase,
24
24
  writeOutput, reportUnchanged,
25
25
  provideService,
@@ -283,13 +283,27 @@ export function layouts(userOptions = {}) {
283
283
  // non-entity, which is why it is a rule and not a guess.
284
284
  const isSidecarScript = (rel) => path.extname(rel.replace(/\.js$/, '')) === ''
285
285
 
286
+ // The folder, resolved the same way the onLoaded below resolves it.
287
+ //
288
+ // That onLoaded is where `runtime.options.layoutsFolder` gets set, and
289
+ // a report-only command — mikser_explain, --audit-output — runs in an
290
+ // onLoaded too. Which of the two runs first is plugin order, so a
291
+ // recompute that read the option directly worked or threw depending on
292
+ // it. Derived here instead, so the answer does not depend on the
293
+ // question's timing.
294
+ function layoutsFolderNow() {
295
+ if (runtime.options.layoutsFolder) return runtime.options.layoutsFolder
296
+ const name = runtime.options.layouts ?? options.layoutsFolder ?? collection
297
+ return path.isAbsolute(name) ? name : path.join(runtime.options.workingFolder, name)
298
+ }
299
+
286
300
  async function sidecarInputs() {
287
- const scriptPaths = (await globby('**/*.js', { cwd: runtime.options.layoutsFolder }))
301
+ const scriptPaths = (await globby('**/*.js', { cwd: layoutsFolderNow() }))
288
302
  .filter(isSidecarScript)
289
303
  const own = new Map()
290
304
  const shared = []
291
305
  for (const rel of scriptPaths.sort()) {
292
- const sum = await fileChecksum(path.join(runtime.options.layoutsFolder, rel))
306
+ const sum = await fileChecksum(path.join(layoutsFolderNow(), rel))
293
307
  own.set(rel.replace(/\.js$/, ''), sum)
294
308
  shared.push(`${rel}:${sum}`)
295
309
  }
@@ -305,6 +319,29 @@ export function layouts(userOptions = {}) {
305
319
  return Buffer.from(`${template}:${sidecar}:${inputs.sharedDigest}`, 'utf8')
306
320
  }
307
321
 
322
+ // Tell core how to recompute what it stored.
323
+ //
324
+ // The catalog holds the composed value above, not a file hash, so
325
+ // anything comparing it against a fresh md5 of the template compares
326
+ // two recipes and always finds them different. mikser_explain did
327
+ // exactly that and reported `differs: true` for every layout on every
328
+ // site — a permanent false alarm about files nobody had edited.
329
+ //
330
+ // The recipe belongs here, so the recomputation does too. One
331
+ // function, the same one the gate uses, so the two cannot drift apart.
332
+ registerSourceChecksum(collection, async (entity) => {
333
+ // A sidecar is in this collection but is NOT composed: it is
334
+ // gated on its own bytes above, because it has no template and no
335
+ // sidecar of its own. Answering with a composed value here would
336
+ // report every sidecar as diverged — the same false alarm this
337
+ // registration exists to remove, pointed the other way.
338
+ //
339
+ // null means "nothing composed here, use the file hash", which is
340
+ // exactly right for it.
341
+ if (entity.type === 'sidecar') return null
342
+ return checksumOf(await layoutInputBytes(entity.uri, entity.name, await sidecarInputs()))
343
+ })
344
+
308
345
  // Named so onSync can re-run it when a sidecar changes. A function
309
346
  // declaration, so it is hoisted above the onSync registration above.
310
347
  async function rescanLayouts() {
@@ -584,6 +621,7 @@ export function layouts(userOptions = {}) {
584
621
  return {
585
622
  collection,
586
623
  type,
624
+ module: import.meta.url,
587
625
  }
588
626
  }
589
627
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io-layouts",
3
- "version": "3.1.0",
3
+ "version": "11.1.0",
4
4
  "description": "SSG-flavor task-production policy for mikser-io. Multi-match layout assignment, per-layout destinations, postprocess chain attachment, layout inspection.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  },
19
19
  "homepage": "https://github.com/almero-digital-marketing/mikser-io-layouts#readme",
20
20
  "peerDependencies": {
21
- "mikser-io": "^10.0.0",
21
+ "mikser-io": "^11.1.0",
22
22
  "zod": "^4.0.0"
23
23
  },
24
24
  "dependencies": {
@@ -27,7 +27,8 @@
27
27
  "lodash": "^4.17.0"
28
28
  },
29
29
  "devDependencies": {
30
- "mikser-io": "file:../mikser-io",
30
+ "mikser-io": "^11.1.0",
31
+ "mikser-io-render-liquid": "^11.0.1",
31
32
  "zod": "^4.0.0"
32
33
  }
33
34
  }