hyper-html-api 0.3.0 → 0.5.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 +60 -0
- package/dist/hyper-html-api.engine.min.js +1 -1
- package/dist/hyper-html-api.min.js +3 -3
- package/dist/hyper-html-api.upgrade.min.js +2 -0
- package/package.json +5 -3
- package/src/adapters/cheerio.js +6 -1
- package/src/adapters/dom.js +6 -1
- package/src/engine/diff.js +3 -1
- package/src/engine/errors.js +0 -7
- package/src/engine/rules-tag.js +1 -1
- package/src/hyper-html-api.js +2 -11
- package/src/upgrade/check.js +116 -0
- package/src/upgrade/errors.js +36 -0
- package/src/upgrade/extract-all.js +26 -0
- package/src/upgrade/index.js +13 -4
- package/src/upgrade/migrate.js +36 -0
- package/src/upgrade/rules-tags.js +37 -0
- package/src/upgrade/run.js +78 -0
- package/src/upgrade/shape-match.js +4 -1
- package/src/upgrade/transform.js +51 -0
- package/src/upgrade.js +14 -0
- package/src/upgrade/helper.js +0 -143
- package/src/upgrade/registry.js +0 -19
- package/src/upgrade/runner.js +0 -92
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load-time update check. Reads the page's hyper-source / hyper-version meta
|
|
3
|
+
* tags, fetches the source anonymously, and compares versions. Results are
|
|
4
|
+
* cached in localStorage for 24 hours, keyed by source + current version so
|
|
5
|
+
* a fresh check happens immediately after an upgrade.
|
|
6
|
+
*
|
|
7
|
+
* Returns null whenever the subsystem should stay inert: missing meta tags,
|
|
8
|
+
* the page IS the canonical source, an unreachable/private source, or
|
|
9
|
+
* unparseable versions. Callers (the popover UI) never need to distinguish
|
|
10
|
+
* these cases.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const CHECK_TTL_MS = 24 * 60 * 60 * 1000
|
|
14
|
+
const CHECK_KEY_PREFIX = 'hha:upgrade-check:'
|
|
15
|
+
|
|
16
|
+
export function readMeta(doc, name) {
|
|
17
|
+
const el = doc.querySelector(`meta[name="${name}"]`)
|
|
18
|
+
const content = el ? el.getAttribute('content') : null
|
|
19
|
+
return content && content.trim() ? content.trim() : null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function parseVersion(v) {
|
|
23
|
+
if (typeof v !== 'string') return null
|
|
24
|
+
const cleaned = v.trim().replace(/^v/, '')
|
|
25
|
+
if (!/^\d+(\.\d+)*$/.test(cleaned)) return null
|
|
26
|
+
return cleaned.split('.').map(Number)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isNewerVersion(candidate, current) {
|
|
30
|
+
const a = parseVersion(candidate)
|
|
31
|
+
const b = parseVersion(current)
|
|
32
|
+
if (!a || !b) return false
|
|
33
|
+
const len = Math.max(a.length, b.length)
|
|
34
|
+
for (let i = 0; i < len; i++) {
|
|
35
|
+
const x = a[i] || 0
|
|
36
|
+
const y = b[i] || 0
|
|
37
|
+
if (x !== y) return x > y
|
|
38
|
+
}
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function storageGet(key) {
|
|
43
|
+
try {
|
|
44
|
+
const raw = globalThis.localStorage.getItem(key)
|
|
45
|
+
return raw ? JSON.parse(raw) : null
|
|
46
|
+
} catch {
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function storageSet(key, value) {
|
|
52
|
+
try {
|
|
53
|
+
globalThis.localStorage.setItem(key, JSON.stringify(value))
|
|
54
|
+
} catch {
|
|
55
|
+
/* storage unavailable (private mode): every check is uncached */
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function checkForUpdate({
|
|
60
|
+
force = false,
|
|
61
|
+
doc,
|
|
62
|
+
loc,
|
|
63
|
+
fetchFn,
|
|
64
|
+
now,
|
|
65
|
+
ttlMs = CHECK_TTL_MS,
|
|
66
|
+
} = {}) {
|
|
67
|
+
doc = doc || (typeof document !== 'undefined' ? document : null)
|
|
68
|
+
loc = loc || (typeof location !== 'undefined' ? location : null)
|
|
69
|
+
fetchFn = fetchFn || ((url) => fetch(url))
|
|
70
|
+
now = now ?? Date.now()
|
|
71
|
+
if (!doc) return null
|
|
72
|
+
|
|
73
|
+
const sourceUrl = readMeta(doc, 'hyper-source')
|
|
74
|
+
if (!sourceUrl) return null
|
|
75
|
+
const currentVersion = readMeta(doc, 'hyper-version')
|
|
76
|
+
|
|
77
|
+
let source
|
|
78
|
+
try {
|
|
79
|
+
source = new URL(sourceUrl, loc ? loc.href : undefined)
|
|
80
|
+
} catch {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
// Self-source guard: the canonical page carries a self-pointing
|
|
84
|
+
// hyper-source; it must never offer to upgrade from itself.
|
|
85
|
+
if (loc && source.origin === loc.origin && source.pathname === loc.pathname) {
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const cacheKey = `${CHECK_KEY_PREFIX}${source.href}:${currentVersion || 'none'}`
|
|
90
|
+
let sourceVersion
|
|
91
|
+
const cached = force ? null : storageGet(cacheKey)
|
|
92
|
+
if (cached && typeof cached.checkedAt === 'number' && now - cached.checkedAt < ttlMs) {
|
|
93
|
+
sourceVersion = cached.sourceVersion
|
|
94
|
+
} else {
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetchFn(source.href)
|
|
97
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
98
|
+
const text = await res.text()
|
|
99
|
+
const sourceDoc = new DOMParser().parseFromString(text, 'text/html')
|
|
100
|
+
sourceVersion = readMeta(sourceDoc, 'hyper-version')
|
|
101
|
+
} catch {
|
|
102
|
+
sourceVersion = null
|
|
103
|
+
}
|
|
104
|
+
storageSet(cacheKey, { checkedAt: now, sourceVersion })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!sourceVersion) return null
|
|
108
|
+
return {
|
|
109
|
+
// A fork without a hyper-version predates versioning: any source version
|
|
110
|
+
// counts as newer.
|
|
111
|
+
available: isNewerVersion(sourceVersion, currentVersion || '0.0.0'),
|
|
112
|
+
currentVersion,
|
|
113
|
+
sourceVersion,
|
|
114
|
+
sourceUrl: source.href,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export class UpgradeSourceUnreachable extends Error {
|
|
2
|
+
constructor(url, cause) {
|
|
3
|
+
super(`could not fetch upgrade source: ${url}`)
|
|
4
|
+
this.name = 'UpgradeSourceUnreachable'
|
|
5
|
+
this.sourceUrl = url
|
|
6
|
+
this.cause = cause
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class UpgradeSourceHasNoRules extends Error {
|
|
11
|
+
constructor(url) {
|
|
12
|
+
super(
|
|
13
|
+
`upgrade source has no rules tags; refusing to drop all existing data: ${url}`,
|
|
14
|
+
)
|
|
15
|
+
this.name = 'UpgradeSourceHasNoRules'
|
|
16
|
+
this.sourceUrl = url
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class UpgradeTransformInvalid extends Error {
|
|
21
|
+
constructor(message, cause) {
|
|
22
|
+
super(message)
|
|
23
|
+
this.name = 'UpgradeTransformInvalid'
|
|
24
|
+
this.cause = cause
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class UpgradeMultipleTransforms extends Error {
|
|
29
|
+
constructor(count) {
|
|
30
|
+
super(
|
|
31
|
+
`found ${count} script[type="text/hyper-upgrade"] tags; only one is allowed per document.`,
|
|
32
|
+
)
|
|
33
|
+
this.name = 'UpgradeMultipleTransforms'
|
|
34
|
+
this.count = count
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { extract } from '../engine/extract.js'
|
|
2
|
+
import domAdapter from '../adapters/dom.js'
|
|
3
|
+
import { collectRulesTags } from './rules-tags.js'
|
|
4
|
+
import { readMeta } from './check.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Keyed extraction: one entry per rules tag under `root`, keyed by the
|
|
8
|
+
* normalized data-rules-name value. This is the v1 side of an upgrade:
|
|
9
|
+
* pack the page's data into labeled boxes.
|
|
10
|
+
*/
|
|
11
|
+
export function extractAllFrom(adapter, root) {
|
|
12
|
+
const dataByName = {}
|
|
13
|
+
for (const { name, rules } of collectRulesTags(adapter, root)) {
|
|
14
|
+
dataByName[name] = extract(adapter, root, rules)
|
|
15
|
+
}
|
|
16
|
+
return dataByName
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function extractAll(root) {
|
|
20
|
+
const r = root || document
|
|
21
|
+
const doc = r.nodeType === 9 ? r : r.ownerDocument || r
|
|
22
|
+
return {
|
|
23
|
+
dataByName: extractAllFrom(domAdapter, r),
|
|
24
|
+
version: readMeta(doc, 'hyper-version'),
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/upgrade/index.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
1
|
+
export { checkForUpdate, readMeta, parseVersion, isNewerVersion } from './check.js'
|
|
2
|
+
export { extractAll, extractAllFrom } from './extract-all.js'
|
|
3
|
+
export { findTransformTag, evaluateTransform, TRANSFORM_TYPE } from './transform.js'
|
|
4
|
+
export { migrateInto } from './migrate.js'
|
|
5
|
+
export { run } from './run.js'
|
|
6
|
+
export { shapeMatch, countScalarLeaves } from './shape-match.js'
|
|
7
|
+
export { collectRulesTags, normalizeName } from './rules-tags.js'
|
|
8
|
+
export {
|
|
9
|
+
UpgradeSourceUnreachable,
|
|
10
|
+
UpgradeSourceHasNoRules,
|
|
11
|
+
UpgradeTransformInvalid,
|
|
12
|
+
UpgradeMultipleTransforms,
|
|
13
|
+
} from './errors.js'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { apply } from '../engine/apply.js'
|
|
2
|
+
import { collectRulesTags } from './rules-tags.js'
|
|
3
|
+
import { shapeMatch, countScalarLeaves } from './shape-match.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pour keyed v1 data into the v2 document. Exact-name join: each v2 rules
|
|
7
|
+
* tag pulls the entry with the same normalized name; tags with no matching
|
|
8
|
+
* entry keep their template defaults; v1 entries with no matching tag are
|
|
9
|
+
* counted as discarded. Renames are the transform's job, not the join's.
|
|
10
|
+
*/
|
|
11
|
+
export function migrateInto(adapter, root, dataByName) {
|
|
12
|
+
const tags = collectRulesTags(adapter, root)
|
|
13
|
+
const totals = { carriedOver: 0, discarded: 0, listItems: 0 }
|
|
14
|
+
const byName = {}
|
|
15
|
+
const v2Names = new Set()
|
|
16
|
+
|
|
17
|
+
for (const { name, rules } of tags) {
|
|
18
|
+
v2Names.add(name)
|
|
19
|
+
if (!Object.prototype.hasOwnProperty.call(dataByName, name)) {
|
|
20
|
+
byName[name] = { carriedOver: 0, discarded: 0, listItems: 0 }
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
const { data, summary } = shapeMatch(dataByName[name], rules)
|
|
24
|
+
apply(adapter, root, rules, data)
|
|
25
|
+
byName[name] = summary
|
|
26
|
+
totals.carriedOver += summary.carriedOver
|
|
27
|
+
totals.discarded += summary.discarded
|
|
28
|
+
totals.listItems += summary.listItems
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const key of Object.keys(dataByName)) {
|
|
32
|
+
if (!v2Names.has(key)) totals.discarded += countScalarLeaves(dataByName[key])
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { totals, byName, rulesTagCount: tags.length }
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { parseRelaxed } from '../engine/rules.js'
|
|
2
|
+
import { SUPPORTED_VERSION } from '../engine/rules-tag.js'
|
|
3
|
+
import { UnknownRulesVersion } from '../engine/errors.js'
|
|
4
|
+
|
|
5
|
+
const RULES_SELECTOR = 'script[data-rules-name]'
|
|
6
|
+
|
|
7
|
+
export function normalizeName(raw) {
|
|
8
|
+
return typeof raw === 'string' ? raw.trim().replace(/\s+/g, ' ') : ''
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Collect every rules tag under `root` as [{ name, rules }], keyed by the
|
|
13
|
+
* whitespace-normalized data-rules-name attribute value. Duplicate names:
|
|
14
|
+
* first tag wins with a warning (mirrors findRulesIn). Unsupported
|
|
15
|
+
* data-rules-version throws so engine-format skew fails loudly instead of
|
|
16
|
+
* corrupting a migration.
|
|
17
|
+
*/
|
|
18
|
+
export function collectRulesTags(adapter, root) {
|
|
19
|
+
const tags = adapter.find(root, RULES_SELECTOR, { includeRulesTag: true })
|
|
20
|
+
const seen = new Set()
|
|
21
|
+
const out = []
|
|
22
|
+
for (const tag of tags) {
|
|
23
|
+
const name = normalizeName(adapter.attr(tag, 'data-rules-name'))
|
|
24
|
+
if (!name) continue
|
|
25
|
+
if (seen.has(name)) {
|
|
26
|
+
console.warn(
|
|
27
|
+
`hyper-html-api: duplicate rules tag name "${name}"; using the first.`,
|
|
28
|
+
)
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
const version = adapter.attr(tag, 'data-rules-version')
|
|
32
|
+
if (version !== SUPPORTED_VERSION) throw new UnknownRulesVersion(version)
|
|
33
|
+
seen.add(name)
|
|
34
|
+
out.push({ name, rules: parseRelaxed(adapter.text(tag)) })
|
|
35
|
+
}
|
|
36
|
+
return out
|
|
37
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent-side upgrade runner (the whole flow, no iframe):
|
|
3
|
+
*
|
|
4
|
+
* 1. Pack: extract keyed data from the live page.
|
|
5
|
+
* 2. Download: fetch the source's pristine HTML, parse with DOMParser
|
|
6
|
+
* (scripts never execute, so no runtime DOM noise can leak into what
|
|
7
|
+
* gets saved).
|
|
8
|
+
* 3. Recipe: find the source's text/hyper-upgrade tag, evaluate it, and
|
|
9
|
+
* reshape the data ({ [name]: data } in, same shape out).
|
|
10
|
+
* 4. Unpack: exact-name join + apply into the parsed copy.
|
|
11
|
+
* 5. Serialize and return. The caller decides how to save — hyperclayjs
|
|
12
|
+
* uses saveHtml(); standalone users do their own thing.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import domAdapter from '../adapters/dom.js'
|
|
16
|
+
import { readMeta } from './check.js'
|
|
17
|
+
import { extractAllFrom } from './extract-all.js'
|
|
18
|
+
import { findTransformTag, evaluateTransform } from './transform.js'
|
|
19
|
+
import { migrateInto } from './migrate.js'
|
|
20
|
+
import { UpgradeSourceUnreachable, UpgradeSourceHasNoRules, UpgradeTransformInvalid } from './errors.js'
|
|
21
|
+
|
|
22
|
+
export async function run({ sourceUrl, dataByName, fromVersion, doc, loc, fetchFn } = {}) {
|
|
23
|
+
doc = doc || (typeof document !== 'undefined' ? document : null)
|
|
24
|
+
loc = loc || (typeof location !== 'undefined' ? location : null)
|
|
25
|
+
fetchFn = fetchFn || ((url) => fetch(url))
|
|
26
|
+
if (!doc) throw new Error('upgrade.run requires a document context')
|
|
27
|
+
|
|
28
|
+
sourceUrl = sourceUrl || readMeta(doc, 'hyper-source')
|
|
29
|
+
if (!sourceUrl) {
|
|
30
|
+
throw new Error('upgrade.run: no sourceUrl given and no hyper-source meta tag found')
|
|
31
|
+
}
|
|
32
|
+
const source = new URL(sourceUrl, loc ? loc.href : undefined)
|
|
33
|
+
fromVersion = fromVersion !== undefined ? fromVersion : readMeta(doc, 'hyper-version')
|
|
34
|
+
dataByName = dataByName || extractAllFrom(domAdapter, doc)
|
|
35
|
+
|
|
36
|
+
let res
|
|
37
|
+
try {
|
|
38
|
+
res = await fetchFn(source.href)
|
|
39
|
+
} catch (err) {
|
|
40
|
+
throw new UpgradeSourceUnreachable(source.href, err)
|
|
41
|
+
}
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new UpgradeSourceUnreachable(source.href, new Error(`HTTP ${res.status}`))
|
|
44
|
+
}
|
|
45
|
+
const text = await res.text()
|
|
46
|
+
const sourceDoc = new DOMParser().parseFromString(text, 'text/html')
|
|
47
|
+
const toVersion = readMeta(sourceDoc, 'hyper-version')
|
|
48
|
+
|
|
49
|
+
let map = dataByName
|
|
50
|
+
let transformApplied = false
|
|
51
|
+
const code = findTransformTag(domAdapter, sourceDoc)
|
|
52
|
+
if (code) {
|
|
53
|
+
const transform = await evaluateTransform(code)
|
|
54
|
+
const out = await transform(map, { fromVersion, toVersion })
|
|
55
|
+
if (!out || typeof out !== 'object' || Array.isArray(out)) {
|
|
56
|
+
throw new UpgradeTransformInvalid('transform must return a plain object map of { rulesName: data }')
|
|
57
|
+
}
|
|
58
|
+
map = out
|
|
59
|
+
transformApplied = true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const { totals, byName, rulesTagCount } = migrateInto(domAdapter, sourceDoc, map)
|
|
63
|
+
if (rulesTagCount === 0 && Object.keys(map).length > 0) {
|
|
64
|
+
throw new UpgradeSourceHasNoRules(source.href)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
html: serializeDocument(sourceDoc),
|
|
69
|
+
fromVersion,
|
|
70
|
+
toVersion,
|
|
71
|
+
summary: { transformApplied, totals, byName },
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function serializeDocument(doc) {
|
|
76
|
+
const name = doc.doctype && doc.doctype.name ? doc.doctype.name : 'html'
|
|
77
|
+
return `<!DOCTYPE ${name}>\n` + doc.documentElement.outerHTML
|
|
78
|
+
}
|
|
@@ -28,6 +28,9 @@ function walk(rule, v1, summary) {
|
|
|
28
28
|
return v1
|
|
29
29
|
}
|
|
30
30
|
if (v1 === undefined || v1 === null) return undefined
|
|
31
|
+
// A scalar slot can't hold an object/array from v1; leave the template
|
|
32
|
+
// default (collectDiscards counts the dropped leaves).
|
|
33
|
+
if (typeof v1 === 'object') return undefined
|
|
31
34
|
summary.carriedOver++
|
|
32
35
|
return v1
|
|
33
36
|
}
|
|
@@ -77,7 +80,7 @@ function collectDiscards(rule, v1, summary) {
|
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
function countScalarLeaves(v) {
|
|
83
|
+
export function countScalarLeaves(v) {
|
|
81
84
|
if (v === null || v === undefined) return 0
|
|
82
85
|
if (Array.isArray(v)) return v.reduce((n, x) => n + countScalarLeaves(x), 0)
|
|
83
86
|
if (typeof v === 'object') {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { UpgradeMultipleTransforms, UpgradeTransformInvalid } from './errors.js'
|
|
2
|
+
|
|
3
|
+
export const TRANSFORM_TYPE = 'text/hyper-upgrade'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The migration recipe is a non-executing script tag in the source HTML:
|
|
7
|
+
*
|
|
8
|
+
* <script type="text/hyper-upgrade">
|
|
9
|
+
* export default (dataByName, { fromVersion, toVersion }) => dataByName
|
|
10
|
+
* </script>
|
|
11
|
+
*
|
|
12
|
+
* Browsers ignore unknown script types, so it never runs on the source page
|
|
13
|
+
* itself; the upgrading fork reads it out of the fetched pristine copy and
|
|
14
|
+
* evaluates it on demand. Returns the tag's code, or null when absent.
|
|
15
|
+
*/
|
|
16
|
+
export function findTransformTag(adapter, root) {
|
|
17
|
+
const tags = adapter.find(root, `script[type="${TRANSFORM_TYPE}"]`)
|
|
18
|
+
if (tags.length === 0) return null
|
|
19
|
+
if (tags.length > 1) throw new UpgradeMultipleTransforms(tags.length)
|
|
20
|
+
const code = adapter.text(tags[0])
|
|
21
|
+
return code || null
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Bundlers rewrite a bare dynamic import() when emitting non-ESM formats; the
|
|
25
|
+
// Function indirection keeps it a native browser import() at runtime.
|
|
26
|
+
const dynamicImport = new Function('url', 'return import(url)')
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Evaluate transform code as a real ES module (Blob URL import). The module
|
|
30
|
+
* must `export default` a function; it may be async. Relative imports won't
|
|
31
|
+
* resolve from a Blob URL; absolute (CDN) imports work.
|
|
32
|
+
*/
|
|
33
|
+
export async function evaluateTransform(code) {
|
|
34
|
+
const blob = new Blob([code], { type: 'text/javascript' })
|
|
35
|
+
const url = URL.createObjectURL(blob)
|
|
36
|
+
let mod
|
|
37
|
+
try {
|
|
38
|
+
mod = await dynamicImport(url)
|
|
39
|
+
} catch (err) {
|
|
40
|
+
throw new UpgradeTransformInvalid(
|
|
41
|
+
`transform failed to load: ${err && err.message ? err.message : err}`,
|
|
42
|
+
err,
|
|
43
|
+
)
|
|
44
|
+
} finally {
|
|
45
|
+
URL.revokeObjectURL(url)
|
|
46
|
+
}
|
|
47
|
+
if (typeof mod.default !== 'function') {
|
|
48
|
+
throw new UpgradeTransformInvalid('transform must `export default` a function')
|
|
49
|
+
}
|
|
50
|
+
return mod.default
|
|
51
|
+
}
|
package/src/upgrade.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Lean upgrade entry: the DOM-bound engine + the template-upgrade flow,
|
|
2
|
+
// WITHOUT cms. This is what `hyper-html-api/upgrade` exports and what
|
|
3
|
+
// hyperclayjs vendors for its `upgrade` module.
|
|
4
|
+
import { engine } from './dom-api.js'
|
|
5
|
+
import { checkForUpdate } from './upgrade/check.js'
|
|
6
|
+
import { run } from './upgrade/run.js'
|
|
7
|
+
import { extractAll } from './upgrade/extract-all.js'
|
|
8
|
+
import { shapeMatch } from './upgrade/shape-match.js'
|
|
9
|
+
|
|
10
|
+
export const upgrade = { checkForUpdate, run, extractAll, shapeMatch }
|
|
11
|
+
export { engine }
|
|
12
|
+
|
|
13
|
+
const HyperHtmlApiUpgrade = { engine, upgrade }
|
|
14
|
+
export default HyperHtmlApiUpgrade
|
package/src/upgrade/helper.js
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Helper-mode boot path. When the library loads in an iframe whose URL
|
|
3
|
-
* includes `?_hyperHtmlApi=upgrade-helper&parentOrigin=...`, it skips its
|
|
4
|
-
* normal idle state and instead participates in the postMessage protocol
|
|
5
|
-
* defined in upgrade-plan.md:
|
|
6
|
-
*
|
|
7
|
-
* 1. Once the iframe document is ready, post `hha:upgrade-ready` to
|
|
8
|
-
* `parentOrigin` with the v2 rules + version + whether a transform
|
|
9
|
-
* was registered.
|
|
10
|
-
* 2. Wait for the parent's `hha:upgrade-data` message carrying v1Data.
|
|
11
|
-
* 3. Run the registered transform (if any), shape-match against v2's
|
|
12
|
-
* rules, then `engine.apply` to the iframe DOM.
|
|
13
|
-
* 4. Serialize `documentElement.outerHTML` and post `hha:upgrade-result`
|
|
14
|
-
* back to the parent.
|
|
15
|
-
*
|
|
16
|
-
* Errors at any step bubble out as `hha:upgrade-error`.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { findRulesIn } from '../engine/rules-tag.js'
|
|
20
|
-
import { extract } from '../engine/extract.js'
|
|
21
|
-
import { apply } from '../engine/apply.js'
|
|
22
|
-
import domAdapter from '../adapters/dom.js'
|
|
23
|
-
import { getRegisteredTransform } from './registry.js'
|
|
24
|
-
import { shapeMatch } from './shape-match.js'
|
|
25
|
-
|
|
26
|
-
const HELPER_QUERY = '_hyperHtmlApi'
|
|
27
|
-
const HELPER_VALUE = 'upgrade-helper'
|
|
28
|
-
const PARENT_ORIGIN_QUERY = 'parentOrigin'
|
|
29
|
-
|
|
30
|
-
export function isHelperMode(loc = typeof location !== 'undefined' ? location : null) {
|
|
31
|
-
if (!loc) return false
|
|
32
|
-
try {
|
|
33
|
-
const params = new URLSearchParams(loc.search)
|
|
34
|
-
return params.get(HELPER_QUERY) === HELPER_VALUE
|
|
35
|
-
} catch {
|
|
36
|
-
return false
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function getParentOrigin(loc = typeof location !== 'undefined' ? location : null) {
|
|
41
|
-
if (!loc) return null
|
|
42
|
-
const params = new URLSearchParams(loc.search)
|
|
43
|
-
return params.get(PARENT_ORIGIN_QUERY)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function bootHelper({ win, doc, parentOrigin } = {}) {
|
|
47
|
-
win = win || (typeof window !== 'undefined' ? window : null)
|
|
48
|
-
doc = doc || (typeof document !== 'undefined' ? document : null)
|
|
49
|
-
if (!win || !doc) return
|
|
50
|
-
parentOrigin = parentOrigin || getParentOrigin(win.location)
|
|
51
|
-
if (!parentOrigin) return
|
|
52
|
-
|
|
53
|
-
const start = () => runHandshake({ win, doc, parentOrigin })
|
|
54
|
-
|
|
55
|
-
if (doc.readyState === 'loading') {
|
|
56
|
-
doc.addEventListener('DOMContentLoaded', start, { once: true })
|
|
57
|
-
} else {
|
|
58
|
-
start()
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function runHandshake({ win, doc, parentOrigin }) {
|
|
63
|
-
let found
|
|
64
|
-
try {
|
|
65
|
-
found = findRulesIn(domAdapter, doc.body)
|
|
66
|
-
} catch (e) {
|
|
67
|
-
return postError(win, parentOrigin, e)
|
|
68
|
-
}
|
|
69
|
-
if (!found) {
|
|
70
|
-
return postError(
|
|
71
|
-
win,
|
|
72
|
-
parentOrigin,
|
|
73
|
-
new Error('helper-mode: no rules tag in v2 document'),
|
|
74
|
-
)
|
|
75
|
-
}
|
|
76
|
-
const rules = found.rules
|
|
77
|
-
const version = readMeta(doc, 'hyper-version')
|
|
78
|
-
const hasTransform = !!getRegisteredTransform()
|
|
79
|
-
|
|
80
|
-
const onMessage = (e) => {
|
|
81
|
-
if (e.source !== win.parent) return
|
|
82
|
-
if (e.origin !== parentOrigin) return
|
|
83
|
-
const msg = e.data
|
|
84
|
-
if (!msg || msg.type !== 'hha:upgrade-data') return
|
|
85
|
-
win.removeEventListener('message', onMessage)
|
|
86
|
-
try {
|
|
87
|
-
const result = runUpgrade({ doc, rules, v1Data: msg.v1Data })
|
|
88
|
-
win.parent.postMessage(
|
|
89
|
-
{ type: 'hha:upgrade-result', html: result.html, summary: result.summary },
|
|
90
|
-
parentOrigin,
|
|
91
|
-
)
|
|
92
|
-
} catch (err) {
|
|
93
|
-
postError(win, parentOrigin, err)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
win.addEventListener('message', onMessage)
|
|
97
|
-
|
|
98
|
-
win.parent.postMessage(
|
|
99
|
-
{ type: 'hha:upgrade-ready', rules, version, hasTransform },
|
|
100
|
-
parentOrigin,
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function runUpgrade({ doc, rules, v1Data }) {
|
|
105
|
-
const transform = getRegisteredTransform()
|
|
106
|
-
let transformed = v1Data
|
|
107
|
-
let didTransform = false
|
|
108
|
-
if (transform) {
|
|
109
|
-
transformed = transform(v1Data)
|
|
110
|
-
didTransform = true
|
|
111
|
-
}
|
|
112
|
-
const { data, summary } = shapeMatch(transformed, rules)
|
|
113
|
-
apply(domAdapter, doc.body, rules, data)
|
|
114
|
-
// Re-extract to confirm the apply landed; surface as part of summary.
|
|
115
|
-
const applied = extract(domAdapter, doc.body, rules)
|
|
116
|
-
const html =
|
|
117
|
-
'<!DOCTYPE html>\n' + doc.documentElement.outerHTML
|
|
118
|
-
return {
|
|
119
|
-
html,
|
|
120
|
-
summary: { ...summary, transformApplied: didTransform, appliedFieldCount: countLeaves(applied) },
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function postError(win, parentOrigin, err) {
|
|
125
|
-
win.parent.postMessage(
|
|
126
|
-
{ type: 'hha:upgrade-error', name: err?.name || 'Error', message: err?.message || String(err) },
|
|
127
|
-
parentOrigin,
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function readMeta(doc, name) {
|
|
132
|
-
const el = doc.querySelector(`meta[name="${name}"]`)
|
|
133
|
-
return el ? el.getAttribute('content') : null
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function countLeaves(v) {
|
|
137
|
-
if (v == null) return 0
|
|
138
|
-
if (Array.isArray(v)) return v.reduce((n, x) => n + countLeaves(x), 0)
|
|
139
|
-
if (typeof v === 'object') {
|
|
140
|
-
return Object.values(v).reduce((n, x) => n + countLeaves(x), 0)
|
|
141
|
-
}
|
|
142
|
-
return 1
|
|
143
|
-
}
|
package/src/upgrade/registry.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { UpgradeAlreadyRegistered } from '../engine/errors.js'
|
|
2
|
-
|
|
3
|
-
let registered = null
|
|
4
|
-
|
|
5
|
-
export function registerUpgrade(fn) {
|
|
6
|
-
if (typeof fn !== 'function') {
|
|
7
|
-
throw new TypeError('registerUpgrade expects a function (v1Data) => v2Data')
|
|
8
|
-
}
|
|
9
|
-
if (registered) throw new UpgradeAlreadyRegistered()
|
|
10
|
-
registered = fn
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function getRegisteredTransform() {
|
|
14
|
-
return registered
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function _resetRegistryForTests() {
|
|
18
|
-
registered = null
|
|
19
|
-
}
|