@pyreon/head 0.21.0 → 0.22.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/lib/analysis/context.js.html +5406 -0
- package/lib/analysis/index.js.html +1 -1
- package/lib/analysis/provider.js.html +1 -1
- package/lib/analysis/ssr.js.html +1 -1
- package/lib/analysis/use-head.js.html +1 -1
- package/lib/context.js +62 -0
- package/lib/index.js +4 -61
- package/lib/provider.js +2 -59
- package/lib/ssr.js +2 -59
- package/lib/types/context.d.ts +205 -0
- package/lib/types/index.d.ts +1 -2
- package/lib/use-head.js +2 -5
- package/package.json +12 -7
- package/src/index.ts +6 -1
- package/src/provider.ts +4 -1
- package/src/ssr.ts +10 -1
- package/src/tests/context-identity.test.ts +150 -0
- package/src/use-head.ts +4 -1
package/src/ssr.ts
CHANGED
|
@@ -2,7 +2,16 @@ import type { ComponentFn, VNode } from '@pyreon/core'
|
|
|
2
2
|
import { h, pushContext } from '@pyreon/core'
|
|
3
3
|
import { renderToString } from '@pyreon/runtime-server'
|
|
4
4
|
import type { HeadTag } from './context'
|
|
5
|
-
|
|
5
|
+
// Runtime VALUE imports go through the self-package path so the build
|
|
6
|
+
// emits an external `import` instead of inlining `createContext(null)`
|
|
7
|
+
// into this sub-bundle. The bundler externalizes `@pyreon/head/context`
|
|
8
|
+
// per the package's `build.external` config — every sub-entry resolves to
|
|
9
|
+
// the SAME `lib/context.js` at runtime, so `HeadContext` is one Symbol
|
|
10
|
+
// across `lib/index.js` / `lib/ssr.js` / `lib/use-head.js` / `lib/provider.js`.
|
|
11
|
+
// See `tests/context-identity.test.ts` for the post-build identity contract.
|
|
12
|
+
// Type-only imports (`HeadTag` above) keep the relative path — types erase
|
|
13
|
+
// at build, no externalization needed.
|
|
14
|
+
import { createHeadContext, HeadContext } from '@pyreon/head/context'
|
|
6
15
|
|
|
7
16
|
const VOID_TAGS = new Set(['meta', 'link', 'base'])
|
|
8
17
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle-level regression: `HeadContext` is constructed in EXACTLY ONE
|
|
3
|
+
* place across the published `lib/` artifacts.
|
|
4
|
+
*
|
|
5
|
+
* The bug this test exists to catch: `@pyreon/head@0.21.0` shipped four
|
|
6
|
+
* sub-entries (`lib/index.js`, `lib/provider.js`, `lib/use-head.js`,
|
|
7
|
+
* `lib/ssr.js`) AND the shared `@vitus-labs/tools-rolldown` build invokes
|
|
8
|
+
* rolldown ONCE PER SUB-ENTRY (no cross-entry shared chunks). Result:
|
|
9
|
+
* every sub-bundle independently inlined `context.ts` and ran its own
|
|
10
|
+
* `createContext(null)` at module init — each call minted a unique
|
|
11
|
+
* `Symbol.for(...).id`, so a `useContext(HeadContext)` lookup in one
|
|
12
|
+
* bundle (e.g. the app's `useHead` from `lib/use-head.js`) silently
|
|
13
|
+
* MISSED a `provide(HeadContext)` from another (e.g. `renderWithHead`
|
|
14
|
+
* from `lib/ssr.js`). The bug was invisible in dev / source-mode tests
|
|
15
|
+
* because Vite's `bun` condition resolves to a single shared
|
|
16
|
+
* `src/context.ts` (ESM single-evaluation guarantee), but SSG output
|
|
17
|
+
* silently dropped every `useHead()`-registered tag — bad for SEO,
|
|
18
|
+
* social scrapers, accessibility, no-JS.
|
|
19
|
+
*
|
|
20
|
+
* The fix (`vl-tools.config.mjs` + self-package imports + the new
|
|
21
|
+
* `./context` sub-export): source uses `@pyreon/head/context` for the
|
|
22
|
+
* runtime VALUE; the build externalizes that specifier; every sub-bundle
|
|
23
|
+
* resolves to the SAME `lib/context.js` at runtime — one Symbol, one
|
|
24
|
+
* shared context.
|
|
25
|
+
*
|
|
26
|
+
* Structural assertions:
|
|
27
|
+
* 1. `lib/context.js` is the ONLY bundle that calls `createContext(`.
|
|
28
|
+
* 2. Every other published sub-bundle (`index`, `ssr`, `use-head`,
|
|
29
|
+
* `provider`) imports `HeadContext` from `@pyreon/head/context`
|
|
30
|
+
* (the external — the bundler's signal that the symbol comes from
|
|
31
|
+
* a shared runtime chunk, not from inlined source).
|
|
32
|
+
*
|
|
33
|
+
* Together these two invariants make the bug class structurally
|
|
34
|
+
* impossible to re-introduce silently — any future regression (e.g.
|
|
35
|
+
* removing the `vl-tools.config.mjs` external, or reverting a source
|
|
36
|
+
* file to a relative `./context` import for the runtime VALUE) flips
|
|
37
|
+
* one of the per-bundle counters and trips the assertion.
|
|
38
|
+
*
|
|
39
|
+
* Bisect-verified at the build artifact:
|
|
40
|
+
* - With the fix: lib/context.js has 1 createContext call site (+ 1
|
|
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.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
48
|
+
import { resolve } from 'node:path'
|
|
49
|
+
import { describe, expect, it } from 'vitest'
|
|
50
|
+
|
|
51
|
+
const PKG_ROOT = resolve(__dirname, '..', '..')
|
|
52
|
+
const LIB_DIR = resolve(PKG_ROOT, 'lib')
|
|
53
|
+
const libExists = existsSync(resolve(LIB_DIR, 'index.js'))
|
|
54
|
+
|
|
55
|
+
const read = (name: string) => readFileSync(resolve(LIB_DIR, name), 'utf8')
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The bundle gate runs only when `lib/` has been built — `bun install`'s
|
|
59
|
+
* postinstall bootstrap rebuilds whenever sources are newer than lib, so
|
|
60
|
+
* in a normal dev session lib is always present. CI installs run the
|
|
61
|
+
* same bootstrap. The `skip` is a defensive escape so the suite doesn't
|
|
62
|
+
* false-fail in a partial worktree state where the user manually
|
|
63
|
+
* deleted lib/.
|
|
64
|
+
*/
|
|
65
|
+
describe.skipIf(!libExists)(
|
|
66
|
+
'@pyreon/head bundle-level HeadContext identity (regression for the SSG-Meta-dropped bug)',
|
|
67
|
+
() => {
|
|
68
|
+
// ── Invariant 1: ONE createContext call across all bundles ───────
|
|
69
|
+
//
|
|
70
|
+
// `lib/context.js` is the canonical single chunk. Each other
|
|
71
|
+
// sub-bundle (`index`, `ssr`, `use-head`, `provider`) should
|
|
72
|
+
// import `HeadContext` from it externally, NOT inline a fresh
|
|
73
|
+
// `createContext(null)` call.
|
|
74
|
+
|
|
75
|
+
it('lib/context.js is the SINGLE bundle that calls createContext()', () => {
|
|
76
|
+
const src = read('context.js')
|
|
77
|
+
// 2 occurrences = the `import { createContext }` line + the actual
|
|
78
|
+
// `createContext(null)` call at module init. This is the SOURCE OF
|
|
79
|
+
// TRUTH for HeadContext's Symbol identity.
|
|
80
|
+
expect(src.match(/createContext/g)?.length ?? 0).toBe(2)
|
|
81
|
+
expect(src).toContain('createContext(null)')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it.each([
|
|
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 ─────────
|
|
100
|
+
//
|
|
101
|
+
// Every non-`context` sub-bundle that USES HeadContext must import
|
|
102
|
+
// it from the externalized `@pyreon/head/context` specifier (the
|
|
103
|
+
// bundler's signal the symbol is shared with `lib/context.js`).
|
|
104
|
+
|
|
105
|
+
it.each([
|
|
106
|
+
['index.js'],
|
|
107
|
+
['provider.js'],
|
|
108
|
+
['use-head.js'],
|
|
109
|
+
['ssr.js'],
|
|
110
|
+
])('lib/%s imports HeadContext via the externalized @pyreon/head/context specifier', (name) => {
|
|
111
|
+
const src = read(name)
|
|
112
|
+
// The bundler emits the import verbatim for externalized specifiers.
|
|
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)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
// ── Invariant 3: the package.json wiring that enables it ─────────
|
|
122
|
+
//
|
|
123
|
+
// Two things must coexist for the externalization to be honored —
|
|
124
|
+
// the `./context` sub-export (so the import has a runtime target),
|
|
125
|
+
// and the `vl-tools.config.mjs` external rule (so the bundler keeps
|
|
126
|
+
// the specifier instead of inlining). Locking these here means a
|
|
127
|
+
// future revert of either side immediately fails the test.
|
|
128
|
+
|
|
129
|
+
it('package.json declares the ./context sub-export', () => {
|
|
130
|
+
const pkg = JSON.parse(read('../package.json')) as {
|
|
131
|
+
exports: Record<string, { bun?: string; import?: string; types?: string }>
|
|
132
|
+
}
|
|
133
|
+
expect(pkg.exports['./context']).toBeDefined()
|
|
134
|
+
expect(pkg.exports['./context']?.import).toBe('./lib/context.js')
|
|
135
|
+
expect(pkg.exports['./context']?.bun).toBe('./src/context.ts')
|
|
136
|
+
})
|
|
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
|
+
},
|
|
150
|
+
)
|
package/src/use-head.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
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
|
-
import
|
|
4
|
+
// Runtime VALUE import via self-package path so the build externalizes
|
|
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'
|
|
5
8
|
import { syncDom } from './dom'
|
|
6
9
|
|
|
7
10
|
/** Cast a strict tag interface to the internal props format, stripping undefined values */
|