@pyreon/head 0.22.0 → 0.23.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/README.md +146 -28
- package/lib/_chunks/use-head-B8n30QMl.js +214 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/index.js +4 -260
- package/lib/provider.js +1 -1
- package/lib/ssr.js +1 -1
- package/lib/types/index.d.ts +2 -1
- package/lib/use-head.js +3 -213
- package/package.json +7 -7
- package/src/index.ts +1 -6
- package/src/provider.ts +1 -4
- package/src/ssr.ts +1 -10
- package/src/tests/context-identity.test.ts +67 -91
- package/src/use-head.ts +1 -4
- package/lib/analysis/context.js.html +0 -5406
- package/lib/analysis/provider.js.html +0 -5406
- package/lib/analysis/ssr.js.html +0 -5406
- package/lib/analysis/use-head.js.html +0 -5406
|
@@ -3,48 +3,46 @@
|
|
|
3
3
|
* place across the published `lib/` artifacts.
|
|
4
4
|
*
|
|
5
5
|
* The bug this test exists to catch: `@pyreon/head@0.21.0` shipped four
|
|
6
|
-
* sub-entries (`lib/index
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
6
|
+
* sub-entries (`lib/{index,provider,use-head,ssr}.js`) and the shared
|
|
7
|
+
* `@vitus-labs/tools-rolldown` (< 2.4.0) invoked rolldown ONCE PER
|
|
8
|
+
* SUB-ENTRY (no cross-entry shared chunks). Result: every sub-bundle
|
|
9
|
+
* independently inlined `context.ts` and ran its own `createContext(null)`
|
|
10
|
+
* at module init — each call minted a unique `Symbol.for(...).id`, so a
|
|
11
|
+
* `useContext(HeadContext)` lookup in one bundle (e.g. the app's
|
|
12
|
+
* `useHead` from `lib/use-head.js`) silently MISSED a
|
|
13
|
+
* `provide(HeadContext)` from another (e.g. `renderWithHead` from
|
|
14
|
+
* `lib/ssr.js`). The bug was invisible in dev / source-mode tests because
|
|
15
|
+
* Vite's `bun` condition resolves to a single shared `src/context.ts`
|
|
16
|
+
* (ESM single-evaluation guarantee), but SSG output silently dropped
|
|
17
|
+
* every `useHead()`-registered tag — bad for SEO, social scrapers,
|
|
18
|
+
* accessibility, no-JS.
|
|
19
19
|
*
|
|
20
|
-
* The fix
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* The durable fix lives upstream in `@vitus-labs/tools-rolldown >= 2.4.0`:
|
|
21
|
+
* the build tool now creates SHARED CHUNKS across sub-entries, so the
|
|
22
|
+
* shared `context.ts` gets hoisted into a single chunk (`lib/context.js`)
|
|
23
|
+
* that every other sub-entry imports via relative-path `./context.js`.
|
|
24
|
+
* `createContext(null)` runs exactly once at runtime; `HeadContext` is
|
|
25
|
+
* one Symbol across every sub-entry's bundle. No per-package
|
|
26
|
+
* externalization / self-package-import workaround needed.
|
|
25
27
|
*
|
|
26
|
-
* Structural assertions:
|
|
28
|
+
* Structural assertions (the BUG-CLASS-LOCK — same intent, cleaner shape):
|
|
27
29
|
* 1. `lib/context.js` is the ONLY bundle that calls `createContext(`.
|
|
28
|
-
* 2.
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* 2. EVERY other published JS file under `lib/` (including
|
|
31
|
+
* `lib/_chunks/*.js` shared chunks the tool emits) has ZERO
|
|
32
|
+
* `createContext` references — they all import `HeadContext` from
|
|
33
|
+
* `./context.js`, sharing the single Symbol identity.
|
|
32
34
|
*
|
|
33
|
-
* Together these
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* import line = 2 occurrences); every other sub-bundle has 0.
|
|
42
|
-
* - Without the fix (revert `vl-tools.config.mjs`): every sub-bundle
|
|
43
|
-
* gets its own inlined `createContext(null)` (2 occurrences each) —
|
|
44
|
-
* this test fails on the first non-context bundle.
|
|
35
|
+
* Together these invariants make the bug class structurally impossible
|
|
36
|
+
* to re-introduce silently — any future regression (e.g. downgrade of
|
|
37
|
+
* the build tool below 2.4.0, or a build-config change that re-enables
|
|
38
|
+
* per-entry inlining) flips one of the per-bundle counters and trips
|
|
39
|
+
* the assertion. Bisect-verified by reverting the
|
|
40
|
+
* `@vitus-labs/tools-rolldown` bump: every non-context sub-bundle gets
|
|
41
|
+
* its own inlined `createContext(null)` call (2 occurrences each), and
|
|
42
|
+
* the second assertion fails on the first non-context bundle.
|
|
45
43
|
*/
|
|
46
44
|
|
|
47
|
-
import { existsSync, readFileSync } from 'node:fs'
|
|
45
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
48
46
|
import { resolve } from 'node:path'
|
|
49
47
|
import { describe, expect, it } from 'vitest'
|
|
50
48
|
|
|
@@ -52,7 +50,21 @@ const PKG_ROOT = resolve(__dirname, '..', '..')
|
|
|
52
50
|
const LIB_DIR = resolve(PKG_ROOT, 'lib')
|
|
53
51
|
const libExists = existsSync(resolve(LIB_DIR, 'index.js'))
|
|
54
52
|
|
|
55
|
-
const read = (
|
|
53
|
+
const read = (rel: string) => readFileSync(resolve(LIB_DIR, rel), 'utf8')
|
|
54
|
+
|
|
55
|
+
/** Every published JS file under lib/ (incl. _chunks/), excluding source maps. */
|
|
56
|
+
function publishedJsFiles(): string[] {
|
|
57
|
+
const out: string[] = []
|
|
58
|
+
for (const entry of readdirSync(LIB_DIR, { withFileTypes: true })) {
|
|
59
|
+
if (entry.isFile() && entry.name.endsWith('.js')) out.push(entry.name)
|
|
60
|
+
else if (entry.isDirectory() && entry.name === '_chunks') {
|
|
61
|
+
for (const sub of readdirSync(resolve(LIB_DIR, entry.name))) {
|
|
62
|
+
if (sub.endsWith('.js')) out.push(`_chunks/${sub}`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out
|
|
67
|
+
}
|
|
56
68
|
|
|
57
69
|
/**
|
|
58
70
|
* The bundle gate runs only when `lib/` has been built — `bun install`'s
|
|
@@ -67,10 +79,9 @@ describe.skipIf(!libExists)(
|
|
|
67
79
|
() => {
|
|
68
80
|
// ── Invariant 1: ONE createContext call across all bundles ───────
|
|
69
81
|
//
|
|
70
|
-
// `lib/context.js` is the canonical single chunk.
|
|
71
|
-
// sub-bundle
|
|
72
|
-
//
|
|
73
|
-
// `createContext(null)` call.
|
|
82
|
+
// `lib/context.js` is the canonical single chunk. Every other
|
|
83
|
+
// sub-bundle / shared chunk should import `HeadContext` from it,
|
|
84
|
+
// NOT inline a fresh `createContext(null)` call.
|
|
74
85
|
|
|
75
86
|
it('lib/context.js is the SINGLE bundle that calls createContext()', () => {
|
|
76
87
|
const src = read('context.js')
|
|
@@ -81,50 +92,27 @@ describe.skipIf(!libExists)(
|
|
|
81
92
|
expect(src).toContain('createContext(null)')
|
|
82
93
|
})
|
|
83
94
|
|
|
84
|
-
|
|
85
|
-
['index.js'],
|
|
86
|
-
['provider.js'],
|
|
87
|
-
['use-head.js'],
|
|
88
|
-
['ssr.js'],
|
|
89
|
-
])('lib/%s does NOT inline createContext (must import HeadContext from @pyreon/head/context)', (name) => {
|
|
90
|
-
const src = read(name)
|
|
91
|
-
// Zero `createContext` references — anything > 0 means the bundle
|
|
92
|
-
// either imports `createContext` (= would call it at init) or
|
|
93
|
-
// declares its own `HeadContext = createContext(...)`. Both are
|
|
94
|
-
// the bug. With the fix, the sub-bundle's HeadContext arrives via
|
|
95
|
-
// an external `import { HeadContext } from "@pyreon/head/context"`.
|
|
96
|
-
expect(src.match(/createContext/g)?.length ?? 0).toBe(0)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
// ── Invariant 2: external import to @pyreon/head/context ─────────
|
|
95
|
+
// ── Invariant 2: ZERO createContext references in EVERY other JS ──
|
|
100
96
|
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
97
|
+
// Covers both the top-level sub-entries AND the `_chunks/*.js` files
|
|
98
|
+
// the build tool now emits — any file that contains `createContext`
|
|
99
|
+
// would be running it at module-init and minting its own Symbol.
|
|
104
100
|
|
|
105
|
-
it
|
|
106
|
-
[
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
// Both ESM string literal styles (`"…"` and `'…'`) are matched
|
|
114
|
-
// because rolldown's output quoting is deterministic but not
|
|
115
|
-
// version-pinned by this test.
|
|
116
|
-
const hasExternalImport =
|
|
117
|
-
/from\s+["']@pyreon\/head\/context["']/.test(src)
|
|
118
|
-
expect(hasExternalImport).toBe(true)
|
|
101
|
+
it('NO other lib/*.js (or lib/_chunks/*.js) calls createContext()', () => {
|
|
102
|
+
const offenders: Array<{ file: string; count: number }> = []
|
|
103
|
+
for (const rel of publishedJsFiles()) {
|
|
104
|
+
if (rel === 'context.js') continue
|
|
105
|
+
const count = read(rel).match(/createContext/g)?.length ?? 0
|
|
106
|
+
if (count > 0) offenders.push({ file: rel, count })
|
|
107
|
+
}
|
|
108
|
+
expect(offenders).toEqual([])
|
|
119
109
|
})
|
|
120
110
|
|
|
121
111
|
// ── Invariant 3: the package.json wiring that enables it ─────────
|
|
122
112
|
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// the specifier instead of inlining). Locking these here means a
|
|
127
|
-
// future revert of either side immediately fails the test.
|
|
113
|
+
// The `./context` sub-export gives `HeadContext` a stable public
|
|
114
|
+
// address. Locking it here means a future revert immediately fails
|
|
115
|
+
// the test.
|
|
128
116
|
|
|
129
117
|
it('package.json declares the ./context sub-export', () => {
|
|
130
118
|
const pkg = JSON.parse(read('../package.json')) as {
|
|
@@ -134,17 +122,5 @@ describe.skipIf(!libExists)(
|
|
|
134
122
|
expect(pkg.exports['./context']?.import).toBe('./lib/context.js')
|
|
135
123
|
expect(pkg.exports['./context']?.bun).toBe('./src/context.ts')
|
|
136
124
|
})
|
|
137
|
-
|
|
138
|
-
it('vl-tools.config.mjs externalizes @pyreon/head/context for every sub-entry build', () => {
|
|
139
|
-
// Plain text read instead of dynamic import — vitest serves test
|
|
140
|
-
// files via http:// URLs, and Node's default ESM loader rejects
|
|
141
|
-
// anything outside `file:` / `data:`. Text-grep is enough: this
|
|
142
|
-
// assertion is structural, not behavioural — the build pipeline
|
|
143
|
-
// already proved the contract by emitting external imports in
|
|
144
|
-
// every sub-bundle (invariants 1 and 2 above).
|
|
145
|
-
const cfg = readFileSync(resolve(PKG_ROOT, 'vl-tools.config.mjs'), 'utf8')
|
|
146
|
-
expect(cfg).toContain("'@pyreon/head/context'")
|
|
147
|
-
expect(cfg).toMatch(/external\s*:/)
|
|
148
|
-
})
|
|
149
125
|
},
|
|
150
126
|
)
|
package/src/use-head.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import { onMount, onUnmount, useContext } from '@pyreon/core'
|
|
2
2
|
import { effect } from '@pyreon/reactivity'
|
|
3
3
|
import type { HeadEntry, HeadTag, UseHeadInput } from './context'
|
|
4
|
-
|
|
5
|
-
// the symbol — every sub-entry resolves to the same `lib/context.js` at
|
|
6
|
-
// runtime. See `ssr.ts` for the full rationale + `tests/context-identity.test.ts`.
|
|
7
|
-
import { HeadContext } from '@pyreon/head/context'
|
|
4
|
+
import { HeadContext } from './context'
|
|
8
5
|
import { syncDom } from './dom'
|
|
9
6
|
|
|
10
7
|
/** Cast a strict tag interface to the internal props format, stripping undefined values */
|