@braidlabs/core 0.7.0 → 0.8.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/package.json +1 -1
- package/src/adapters/compat-adapter.js +51 -4
- package/src/adapters/compat-adapter.js.map +1 -1
- package/src/compat/database-partition.d.ts +43 -0
- package/src/compat/database-partition.js +165 -0
- package/src/compat/database-partition.js.map +1 -0
- package/src/compat/fragment-boundary.js +133 -25
- package/src/compat/fragment-boundary.js.map +1 -1
- package/src/compat/html-policy.js +10 -5
- package/src/compat/html-policy.js.map +1 -1
- package/src/compat/script-execution.js +5 -4
- package/src/compat/script-execution.js.map +1 -1
- package/src/compat/shared-globals.d.ts +1 -1
- package/src/compat/shared-globals.js +2 -2
- package/src/compat/shared-globals.js.map +1 -1
- package/src/config.d.ts +13 -2
- package/src/config.js.map +1 -1
- package/src/elements/fragment-slot.d.ts +41 -15
- package/src/elements/fragment-slot.js +195 -9
- package/src/elements/fragment-slot.js.map +1 -1
- package/src/elements/leave-guard.d.ts +49 -0
- package/src/elements/leave-guard.js +122 -0
- package/src/elements/leave-guard.js.map +1 -0
- package/src/elements/overlay-layer.d.ts +45 -0
- package/src/elements/overlay-layer.js +112 -0
- package/src/elements/overlay-layer.js.map +1 -0
- package/src/env/create-env.js +2 -1
- package/src/env/create-env.js.map +1 -1
- package/src/env/fragment-env.d.ts +6 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +14 -0
- package/src/index.js.map +1 -1
- package/src/realm/realm-manager.js +2 -1
- package/src/realm/realm-manager.js.map +1 -1
- package/src/realm/sandbox-realm.d.ts +5 -7
- package/src/realm/sandbox-realm.js +104 -39
- package/src/realm/sandbox-realm.js.map +1 -1
- package/src/trusted-types.d.ts +28 -0
- package/src/trusted-types.js +58 -0
- package/src/trusted-types.js.map +1 -0
- package/src/weave/capabilities.d.ts +10 -3
- package/src/weave/capabilities.js +19 -3
- package/src/weave/capabilities.js.map +1 -1
- package/src/weave/context-bridge.d.ts +2 -0
- package/src/weave/context-bridge.js +1 -1
- package/src/weave/context-bridge.js.map +1 -1
- package/src/weave/guest.d.ts +2 -0
- package/src/weave/guest.js +10 -5
- package/src/weave/guest.js.map +1 -1
- package/src/weave/messages.d.ts +24 -0
- package/src/weave/messages.js +22 -0
- package/src/weave/messages.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@braidlabs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Braid client runtime — compose independently deployed frontend apps into one page. This build ships the compat adapter (the contained web-fragments-style emulation layer) as the default adapter.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"micro-frontends",
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { compatMetadataSymbol } from '../compat/metadata.js';
|
|
2
2
|
import { initializeRealmContext } from '../compat/realm-patches.js';
|
|
3
|
-
import { executeScriptsInFragmentContent } from '../compat/script-execution.js';
|
|
3
|
+
import { executeScriptsInFragmentContent, markStartedInPlace } from '../compat/script-execution.js';
|
|
4
4
|
import { installRealmGlobals } from '../compat/shared-globals.js';
|
|
5
5
|
import { isDevMode } from '../config.js';
|
|
6
|
+
import { trustedHTML, trustedScript } from '../trusted-types.js';
|
|
6
7
|
/**
|
|
7
8
|
* The compat adapter — the emulation layer, contained.
|
|
8
9
|
*
|
|
@@ -88,17 +89,63 @@ export const compatAdapter = {
|
|
|
88
89
|
* saw a bare markup snippet.
|
|
89
90
|
*/
|
|
90
91
|
export function parseFragmentContent(html, mainDocument) {
|
|
91
|
-
|
|
92
|
+
// markup the gateway prepared, parsed where nothing runs: Braid's own write (see trusted-types.ts)
|
|
93
|
+
const parsed = new DOMParser().parseFromString(trustedHTML(html), 'text/html');
|
|
92
94
|
const braidHtml = parsed.body.querySelector('braid-html');
|
|
93
95
|
if (braidHtml) {
|
|
94
|
-
return mainDocument.importNode(braidHtml, true);
|
|
96
|
+
return readyScripts(mainDocument.importNode(braidHtml, true));
|
|
95
97
|
}
|
|
96
98
|
const wrapper = mainDocument.createElement('braid-html');
|
|
97
99
|
const braidBody = mainDocument.createElement('braid-body');
|
|
98
100
|
braidBody.append(...[...parsed.body.childNodes].map((node) => mainDocument.importNode(node, true)));
|
|
99
101
|
wrapper.append(braidBody);
|
|
100
|
-
return wrapper;
|
|
102
|
+
return readyScripts(wrapper);
|
|
101
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Readies the fragment's scripts in markup imported from the parser's document, before any of it
|
|
106
|
+
* reaches the page.
|
|
107
|
+
*
|
|
108
|
+
* - **Only what the gateway neutralized is kept.** The gateway gives every script `type="inert"`,
|
|
109
|
+
* an SVG one included; one without it slipped past the rewriter, and nothing here can tell what
|
|
110
|
+
* it would do in the host, so it is dropped (in dev, with a warning).
|
|
111
|
+
* - **Each inline script gets its own text back through Braid's Trusted Types policy.** Imported
|
|
112
|
+
* from another document, a script no longer counts as parser-written, and a host enforcing
|
|
113
|
+
* Trusted Types would refuse to mark it started below. Without a policy nothing changes.
|
|
114
|
+
* - **Each is marked started now, while the tree is detached**, so there is no moment at which a
|
|
115
|
+
* blessed script is in the page unmarked. The fragment's realm runs fresh copies; these never run.
|
|
116
|
+
*/
|
|
117
|
+
function readyScripts(root) {
|
|
118
|
+
for (const script of root.querySelectorAll('script')) {
|
|
119
|
+
if (script.getAttribute('type') !== 'inert') {
|
|
120
|
+
if (isDevMode())
|
|
121
|
+
console.warn('[braid] a fragment script was not neutralized by the gateway and is dropped', script);
|
|
122
|
+
script.remove();
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (script.hasAttribute('src'))
|
|
126
|
+
continue;
|
|
127
|
+
const text = script.textContent ?? '';
|
|
128
|
+
const trusted = trustedScript(text);
|
|
129
|
+
if (trusted === text)
|
|
130
|
+
continue;
|
|
131
|
+
if (script.namespaceURI === XHTML) {
|
|
132
|
+
script.textContent = trusted;
|
|
133
|
+
}
|
|
134
|
+
else if (!script.hasAttribute('href') && !script.hasAttribute('xlink:href')) {
|
|
135
|
+
// An SVG script's text cannot be given back through a policy (only an HTML script's setters
|
|
136
|
+
// take one), so a host enforcing Trusted Types would report it. Its realm runs an HTML copy
|
|
137
|
+
// whatever it is here, so the inert one in the page becomes an HTML script too.
|
|
138
|
+
const html = root.ownerDocument.createElement('script');
|
|
139
|
+
for (const attribute of script.attributes)
|
|
140
|
+
html.setAttribute(attribute.name, attribute.value);
|
|
141
|
+
html.textContent = trusted;
|
|
142
|
+
script.replaceWith(html);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
markStartedInPlace(root);
|
|
146
|
+
return root;
|
|
147
|
+
}
|
|
148
|
+
const XHTML = 'http://www.w3.org/1999/xhtml';
|
|
102
149
|
/**
|
|
103
150
|
* Resolves once every image that holds a document's load event has loaded or failed.
|
|
104
151
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compat-adapter.js","sourceRoot":"","sources":["../../../../../libs/core/src/adapters/compat-adapter.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC/E,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,+BAA+B,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"compat-adapter.js","sourceRoot":"","sources":["../../../../../libs/core/src/adapters/compat-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-fragment IndexedDB and Cache Storage, for a compat fragment granted
|
|
3
|
+
* `capabilities.storage: { databases: true }`.
|
|
4
|
+
*
|
|
5
|
+
* A compat realm is a same-origin iframe, so its `indexedDB` and `caches` are the origin's: two
|
|
6
|
+
* fragments that both open a database called `app` open the same one, and the host sees it too.
|
|
7
|
+
* `compat/storage-partition.ts` already gives each partition its own Web Storage and cookies; this
|
|
8
|
+
* does the same for the databases.
|
|
9
|
+
*
|
|
10
|
+
* - **Where the browser has Storage Buckets** (Chromium), the realm gets a bucket of its own —
|
|
11
|
+
* `navigator.storageBuckets.open()` — and that bucket's `indexedDB` and `caches`. The browser
|
|
12
|
+
* keeps the data apart, and evicts, quotas and deletes it per bucket, so one fragment filling its
|
|
13
|
+
* store does not cost another its data.
|
|
14
|
+
* - **Elsewhere**, the realm's own factory and cache storage are wrapped to prefix every name with
|
|
15
|
+
* the partition, and to list only the partition's own. `IDBDatabase.name` and a cache's key
|
|
16
|
+
* still show the prefix: an accident barrier, like the rest of the trusted tier, not a boundary.
|
|
17
|
+
*
|
|
18
|
+
* Installed before any fragment code runs. Opt-in, so a fragment's existing databases stay where
|
|
19
|
+
* they are until it asks.
|
|
20
|
+
*/
|
|
21
|
+
export type DatabasePartitionKind = 'bucket' | 'prefixed';
|
|
22
|
+
/** Gives the realm window its partition's `indexedDB` and `caches`; resolves with which kind. */
|
|
23
|
+
export declare function installDatabasePartition(realm: Window, partition: string): Promise<DatabasePartitionKind>;
|
|
24
|
+
/**
|
|
25
|
+
* A bucket name for a partition: lowercase letters, digits, `-` and `_`, at most 64 characters, as
|
|
26
|
+
* the API requires. The readable part may collide after that folding; the hash of the exact
|
|
27
|
+
* partition keeps two partitions apart.
|
|
28
|
+
*/
|
|
29
|
+
export declare function bucketName(partition: string): string;
|
|
30
|
+
/** An `IDBFactory` whose databases are the partition's: same prototype, so `instanceof` holds. */
|
|
31
|
+
export declare function prefixedFactory(native: IDBFactory, prefix: string): IDBFactory;
|
|
32
|
+
/** A `CacheStorage` whose caches are the partition's. `match` looks only in those. */
|
|
33
|
+
export declare function prefixedCacheStorage(native: CacheStorage, prefix: string): CacheStorage;
|
|
34
|
+
/**
|
|
35
|
+
* Deletes every fragment's partitioned IndexedDB and Cache Storage on this origin — the buckets,
|
|
36
|
+
* and the prefixed databases and caches of a browser without them. For sign-out on a shared device:
|
|
37
|
+
* a partitioned fragment's data lives where the origin's own `indexedDB.databases()` and
|
|
38
|
+
* `caches.keys()` do not list it, so a sign-out routine that clears those leaves it behind.
|
|
39
|
+
*
|
|
40
|
+
* Only Braid's own: realm buckets matched by their full name format, databases and caches by their
|
|
41
|
+
* `braid:<partition>:` prefix.
|
|
42
|
+
*/
|
|
43
|
+
export declare function clearBraidDatabases(scope?: Window): Promise<void>;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-fragment IndexedDB and Cache Storage, for a compat fragment granted
|
|
3
|
+
* `capabilities.storage: { databases: true }`.
|
|
4
|
+
*
|
|
5
|
+
* A compat realm is a same-origin iframe, so its `indexedDB` and `caches` are the origin's: two
|
|
6
|
+
* fragments that both open a database called `app` open the same one, and the host sees it too.
|
|
7
|
+
* `compat/storage-partition.ts` already gives each partition its own Web Storage and cookies; this
|
|
8
|
+
* does the same for the databases.
|
|
9
|
+
*
|
|
10
|
+
* - **Where the browser has Storage Buckets** (Chromium), the realm gets a bucket of its own —
|
|
11
|
+
* `navigator.storageBuckets.open()` — and that bucket's `indexedDB` and `caches`. The browser
|
|
12
|
+
* keeps the data apart, and evicts, quotas and deletes it per bucket, so one fragment filling its
|
|
13
|
+
* store does not cost another its data.
|
|
14
|
+
* - **Elsewhere**, the realm's own factory and cache storage are wrapped to prefix every name with
|
|
15
|
+
* the partition, and to list only the partition's own. `IDBDatabase.name` and a cache's key
|
|
16
|
+
* still show the prefix: an accident barrier, like the rest of the trusted tier, not a boundary.
|
|
17
|
+
*
|
|
18
|
+
* Installed before any fragment code runs. Opt-in, so a fragment's existing databases stay where
|
|
19
|
+
* they are until it asks.
|
|
20
|
+
*/
|
|
21
|
+
/** Gives the realm window its partition's `indexedDB` and `caches`; resolves with which kind. */
|
|
22
|
+
export async function installDatabasePartition(realm, partition) {
|
|
23
|
+
const realmWindow = realm;
|
|
24
|
+
const buckets = realmWindow.navigator.storageBuckets;
|
|
25
|
+
const prefix = `braid:${encodeURIComponent(partition)}:`;
|
|
26
|
+
// Where a partition's data already is, it stays: a browser that gains buckets, or a bucket that
|
|
27
|
+
// was refused once, would otherwise hand the fragment an empty store and strand the old one.
|
|
28
|
+
if (typeof buckets?.open === 'function' && !(await hasPrefixedData(realmWindow, prefix))) {
|
|
29
|
+
try {
|
|
30
|
+
const bucket = await buckets.open(bucketName(partition));
|
|
31
|
+
define(realm, 'indexedDB', bucket.indexedDB);
|
|
32
|
+
define(realm, 'caches', bucket.caches);
|
|
33
|
+
return 'bucket';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// refused (an opaque origin, a quota policy): fall back to prefixing
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (realmWindow.indexedDB)
|
|
40
|
+
define(realm, 'indexedDB', prefixedFactory(realmWindow.indexedDB, prefix));
|
|
41
|
+
if (realmWindow.caches)
|
|
42
|
+
define(realm, 'caches', prefixedCacheStorage(realmWindow.caches, prefix));
|
|
43
|
+
return 'prefixed';
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A bucket name for a partition: lowercase letters, digits, `-` and `_`, at most 64 characters, as
|
|
47
|
+
* the API requires. The readable part may collide after that folding; the hash of the exact
|
|
48
|
+
* partition keeps two partitions apart.
|
|
49
|
+
*/
|
|
50
|
+
export function bucketName(partition) {
|
|
51
|
+
const readable = partition.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
|
|
52
|
+
return `${BUCKET_NAMESPACE}${readable || 'p'}_${fnv1a(partition)}`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Realm database buckets, and only them: a fixed namespace no other bucket of Braid's uses (the
|
|
56
|
+
* service worker's are `braid-sw-…`), matched in full, so clearing never takes an app's own bucket.
|
|
57
|
+
*/
|
|
58
|
+
const BUCKET_NAMESPACE = 'braid-db_';
|
|
59
|
+
const BUCKET_FORMAT = /^braid-db_[a-z0-9-]{1,40}_[0-9a-f]{8}$/;
|
|
60
|
+
const PREFIX_FORMAT = /^braid:[^:]+:/;
|
|
61
|
+
function fnv1a(text) {
|
|
62
|
+
let hash = 0x811c9dc5;
|
|
63
|
+
for (let i = 0; i < text.length; i++) {
|
|
64
|
+
hash ^= text.charCodeAt(i);
|
|
65
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
66
|
+
}
|
|
67
|
+
return hash.toString(16).padStart(8, '0');
|
|
68
|
+
}
|
|
69
|
+
function define(realm, name, value) {
|
|
70
|
+
Object.defineProperty(realm, name, { configurable: true, enumerable: true, get: () => value });
|
|
71
|
+
}
|
|
72
|
+
/** An `IDBFactory` whose databases are the partition's: same prototype, so `instanceof` holds. */
|
|
73
|
+
export function prefixedFactory(native, prefix) {
|
|
74
|
+
return Object.create(Object.getPrototypeOf(native), {
|
|
75
|
+
open: {
|
|
76
|
+
value: (name, version) => (version === undefined ? native.open(prefix + name) : native.open(prefix + name, version)),
|
|
77
|
+
},
|
|
78
|
+
deleteDatabase: { value: (name) => native.deleteDatabase(prefix + name) },
|
|
79
|
+
// only where the native factory has it, so feature detection tells the truth
|
|
80
|
+
...(typeof native.databases === 'function'
|
|
81
|
+
? {
|
|
82
|
+
databases: {
|
|
83
|
+
value: async () => (await native.databases())
|
|
84
|
+
.filter((info) => info.name?.startsWith(prefix))
|
|
85
|
+
.map((info) => ({ ...info, name: info.name.slice(prefix.length) })),
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
: // shadowing the prototype's, which would throw on this object anyway
|
|
89
|
+
{ databases: { value: undefined } }),
|
|
90
|
+
cmp: { value: (first, second) => native.cmp(first, second) },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** A `CacheStorage` whose caches are the partition's. `match` looks only in those. */
|
|
94
|
+
export function prefixedCacheStorage(native, prefix) {
|
|
95
|
+
const own = async () => (await native.keys()).filter((name) => name.startsWith(prefix));
|
|
96
|
+
return Object.create(Object.getPrototypeOf(native), {
|
|
97
|
+
open: { value: (name) => native.open(prefix + name) },
|
|
98
|
+
has: { value: (name) => native.has(prefix + name) },
|
|
99
|
+
delete: { value: (name) => native.delete(prefix + name) },
|
|
100
|
+
keys: { value: async () => (await own()).map((name) => name.slice(prefix.length)) },
|
|
101
|
+
match: {
|
|
102
|
+
value: async (request, options) => {
|
|
103
|
+
if (options?.cacheName !== undefined)
|
|
104
|
+
return native.match(request, { ...options, cacheName: prefix + options.cacheName });
|
|
105
|
+
for (const name of await own()) {
|
|
106
|
+
const found = await (await native.open(name)).match(request, options);
|
|
107
|
+
if (found)
|
|
108
|
+
return found;
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Whether the partition already keeps databases or caches under prefixed names. */
|
|
116
|
+
async function hasPrefixedData(realm, prefix) {
|
|
117
|
+
const has = async (names) => {
|
|
118
|
+
try {
|
|
119
|
+
return (await names()).some((name) => name?.startsWith(prefix));
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const factory = realm.indexedDB;
|
|
126
|
+
const caches = realm.caches;
|
|
127
|
+
return ((typeof factory?.databases === 'function' && (await has(async () => (await factory.databases()).map((info) => info.name)))) ||
|
|
128
|
+
(caches !== undefined && (await has(() => caches.keys()))));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Deletes every fragment's partitioned IndexedDB and Cache Storage on this origin — the buckets,
|
|
132
|
+
* and the prefixed databases and caches of a browser without them. For sign-out on a shared device:
|
|
133
|
+
* a partitioned fragment's data lives where the origin's own `indexedDB.databases()` and
|
|
134
|
+
* `caches.keys()` do not list it, so a sign-out routine that clears those leaves it behind.
|
|
135
|
+
*
|
|
136
|
+
* Only Braid's own: realm buckets matched by their full name format, databases and caches by their
|
|
137
|
+
* `braid:<partition>:` prefix.
|
|
138
|
+
*/
|
|
139
|
+
export async function clearBraidDatabases(scope = window) {
|
|
140
|
+
const realmWindow = scope;
|
|
141
|
+
const buckets = realmWindow.navigator.storageBuckets;
|
|
142
|
+
if (buckets && typeof buckets.keys === 'function') {
|
|
143
|
+
for (const name of await buckets.keys())
|
|
144
|
+
if (BUCKET_FORMAT.test(name))
|
|
145
|
+
await buckets.delete(name);
|
|
146
|
+
}
|
|
147
|
+
const factory = realmWindow.indexedDB;
|
|
148
|
+
if (factory && typeof factory.databases === 'function') {
|
|
149
|
+
for (const { name } of await factory.databases()) {
|
|
150
|
+
if (name && PREFIX_FORMAT.test(name)) {
|
|
151
|
+
await new Promise((resolve) => {
|
|
152
|
+
const request = factory.deleteDatabase(name);
|
|
153
|
+
request.onsuccess = request.onerror = request.onblocked = () => resolve();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const cacheStorage = realmWindow.caches;
|
|
159
|
+
if (cacheStorage) {
|
|
160
|
+
for (const name of await cacheStorage.keys())
|
|
161
|
+
if (PREFIX_FORMAT.test(name))
|
|
162
|
+
await cacheStorage.delete(name);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=database-partition.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database-partition.js","sourceRoot":"","sources":["../../../../../libs/core/src/compat/database-partition.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAeH,iGAAiG;AACjG,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,KAAa,EAAE,SAAiB;IAC7E,MAAM,WAAW,GAAG,KAAmC,CAAC;IACxD,MAAM,OAAO,GAAG,WAAW,CAAC,SAAS,CAAC,cAAc,CAAC;IACrD,MAAM,MAAM,GAAG,SAAS,kBAAkB,CAAC,SAAS,CAAC,GAAG,CAAC;IACzD,gGAAgG;IAChG,6FAA6F;IAC7F,IAAI,OAAO,OAAO,EAAE,IAAI,KAAK,UAAU,IAAI,CAAC,CAAC,MAAM,eAAe,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC;QACzF,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;YACzD,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC;YAC7C,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;YACvC,OAAO,QAAQ,CAAC;QAClB,CAAC;QAAC,MAAM,CAAC;YACP,qEAAqE;QACvE,CAAC;IACH,CAAC;IACD,IAAI,WAAW,CAAC,SAAS;QAAE,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,eAAe,CAAC,WAAW,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;IACtG,IAAI,WAAW,CAAC,MAAM;QAAE,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,oBAAoB,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClG,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,SAAiB;IAC1C,MAAM,QAAQ,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC3G,OAAO,GAAG,gBAAgB,GAAG,QAAQ,IAAI,GAAG,IAAI,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;AACrE,CAAC;AAED;;;GAGG;AACH,MAAM,gBAAgB,GAAG,WAAW,CAAC;AACrC,MAAM,aAAa,GAAG,wCAAwC,CAAC;AAC/D,MAAM,aAAa,GAAG,eAAe,CAAC;AAEtC,SAAS,KAAK,CAAC,IAAY;IACzB,IAAI,IAAI,GAAG,UAAU,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,IAAI,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC3B,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,MAAM,CAAC,KAAa,EAAE,IAA4B,EAAE,KAAc;IACzE,MAAM,CAAC,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;AACjG,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,eAAe,CAAC,MAAkB,EAAE,MAAc;IAChE,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE;QAClD,IAAI,EAAE;YACJ,KAAK,EAAE,CAAC,IAAY,EAAE,OAAgB,EAAE,EAAE,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;SACtI;QACD,cAAc,EAAE,EAAE,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE;QACjF,6EAA6E;QAC7E,GAAG,CAAC,OAAO,MAAM,CAAC,SAAS,KAAK,UAAU;YACxC,CAAC,CAAC;gBACE,SAAS,EAAE;oBACT,KAAK,EAAE,KAAK,IAAI,EAAE,CAChB,CAAC,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC;yBACvB,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;yBAC/C,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAK,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;iBACzE;aACF;YACH,CAAC,CAAC,qEAAqE;gBACrE,EAAE,SAAS,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC;QACxC,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,KAAc,EAAE,MAAe,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE;KAC/E,CAAe,CAAC;AACnB,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,oBAAoB,CAAC,MAAoB,EAAE,MAAc;IACvE,MAAM,GAAG,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;IACxF,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE;QAClD,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE;QAC7D,GAAG,EAAE,EAAE,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE;QAC3D,MAAM,EAAE,EAAE,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE;QACjE,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE;QACnF,KAAK,EAAE;YACL,KAAK,EAAE,KAAK,EAAE,OAA0B,EAAE,OAAgC,EAAE,EAAE;gBAC5E,IAAI,OAAO,EAAE,SAAS,KAAK,SAAS;oBAAE,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;gBAC1H,KAAK,MAAM,IAAI,IAAI,MAAM,GAAG,EAAE,EAAE,CAAC;oBAC/B,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;oBACtE,IAAI,KAAK;wBAAE,OAAO,KAAK,CAAC;gBAC1B,CAAC;gBACD,OAAO,SAAS,CAAC;YACnB,CAAC;SACF;KACF,CAAiB,CAAC;AACrB,CAAC;AAED,oFAAoF;AACpF,KAAK,UAAU,eAAe,CAAC,KAAsB,EAAE,MAAc;IACnE,MAAM,GAAG,GAAG,KAAK,EAAE,KAAqD,EAAE,EAAE;QAC1E,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QAClE,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC;IACF,MAAM,OAAO,GAAG,KAAK,CAAC,SAAS,CAAC;IAChC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;IAC5B,OAAO,CACL,CAAC,OAAO,OAAO,EAAE,SAAS,KAAK,UAAU,IAAI,CAAC,MAAM,GAAG,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3H,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAC3D,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,QAAgB,MAAM;IAC9D,MAAM,WAAW,GAAG,KAEnB,CAAC;IACF,MAAM,OAAO,GAAG,WAAW,CAAC,SAAS,CAAC,cAAc,CAAC;IACrD,IAAI,OAAO,IAAI,OAAO,OAAO,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAClD,KAAK,MAAM,IAAI,IAAI,MAAM,OAAO,CAAC,IAAI,EAAE;YAAE,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,MAAM,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACpG,CAAC;IACD,MAAM,OAAO,GAAG,WAAW,CAAC,SAAS,CAAC;IACtC,IAAI,OAAO,IAAI,OAAO,OAAO,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;QACvD,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,MAAM,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC;YACjD,IAAI,IAAI,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrC,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;oBAClC,MAAM,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;oBAC7C,OAAO,CAAC,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,OAAO,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,OAAO,EAAE,CAAC;gBAC5E,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IACD,MAAM,YAAY,GAAG,WAAW,CAAC,MAAM,CAAC;IACxC,IAAI,YAAY,EAAE,CAAC;QACjB,KAAK,MAAM,IAAI,IAAI,MAAM,YAAY,CAAC,IAAI,EAAE;YAAE,IAAI,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC;gBAAE,MAAM,YAAY,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC9G,CAAC;AACH,CAAC"}
|
|
@@ -48,12 +48,112 @@ const native = {
|
|
|
48
48
|
elementAfter: Element.prototype.after,
|
|
49
49
|
shadowRootInnerHTML: Object.getOwnPropertyDescriptor(ShadowRoot.prototype, 'innerHTML'),
|
|
50
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* Every boundary's stamped prototypes and installed wrappers, page-wide. A node that moves between
|
|
53
|
+
* fragments carries a stamp from each, and delegation has to see past all of them.
|
|
54
|
+
*/
|
|
55
|
+
const allStampedProtos = new WeakSet();
|
|
56
|
+
const allBoundaryWrappers = new WeakSet();
|
|
57
|
+
/**
|
|
58
|
+
* The built-in prototypes the intercepted methods are defined on. A method found on one of these is
|
|
59
|
+
* the platform's — or a host page's later patch of it, which fragment nodes have never run.
|
|
60
|
+
*/
|
|
61
|
+
const PLATFORM_PROTOTYPES = new Set([Node.prototype, Element.prototype, HTMLElement.prototype, DocumentFragment.prototype]);
|
|
62
|
+
/**
|
|
63
|
+
* The method a node would have without any boundary's stamp: whatever its prototype chain defines
|
|
64
|
+
* below the lowest stamp, read at call time — or the native method, when that is the platform's.
|
|
65
|
+
*
|
|
66
|
+
* A custom element may override insertion on its own class — Stencil's scoped components do, to
|
|
67
|
+
* move light-DOM children into the element's inner slot. The stamp sits above that class, so
|
|
68
|
+
* calling the native method unconditionally would skip the override: children land on the host
|
|
69
|
+
* instead of in the slot, and an `insertBefore` whose reference the component had relocated throws
|
|
70
|
+
* `NotFoundError`.
|
|
71
|
+
*
|
|
72
|
+
* Past the *lowest* stamp, of any boundary, so a node stamped by fragment A, then B, then A again
|
|
73
|
+
* never delegates from one boundary's wrapper to another's and back. Unstamped receivers (a shadow
|
|
74
|
+
* root's own-property overrides) get the native method.
|
|
75
|
+
*/
|
|
76
|
+
function unstampedMethod(node, name, nativeMethod) {
|
|
77
|
+
let belowStamps;
|
|
78
|
+
for (let proto = Object.getPrototypeOf(node); proto !== null; proto = Object.getPrototypeOf(proto)) {
|
|
79
|
+
if (allStampedProtos.has(proto))
|
|
80
|
+
belowStamps = Object.getPrototypeOf(proto);
|
|
81
|
+
}
|
|
82
|
+
for (let owner = belowStamps; owner; owner = Object.getPrototypeOf(owner)) {
|
|
83
|
+
if (!Object.hasOwn(owner, name))
|
|
84
|
+
continue;
|
|
85
|
+
if (PLATFORM_PROTOTYPES.has(owner))
|
|
86
|
+
return nativeMethod;
|
|
87
|
+
const method = Object.getOwnPropertyDescriptor(owner, name)?.value;
|
|
88
|
+
return typeof method === 'function' && !allBoundaryWrappers.has(method) ? method : nativeMethod;
|
|
89
|
+
}
|
|
90
|
+
return nativeMethod;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Custom elements stamped before their definition existed, page-wide by tag name, and what restamps
|
|
94
|
+
* each one.
|
|
95
|
+
*
|
|
96
|
+
* Upgrading an element replaces its prototype with the definition's, which drops the stamp. Most
|
|
97
|
+
* upgrades happen during an insertion the boundary stamps after anyway; this catches the rest — an
|
|
98
|
+
* element already in a fragment's DOM when its definition arrives *in the host's registry*, which is
|
|
99
|
+
* the one that upgrades fragment DOM: a shared component bundle the host loads. A compat fragment's
|
|
100
|
+
* own `customElements.define` goes to its realm's registry and never resolves here.
|
|
101
|
+
*
|
|
102
|
+
* Many dashed names are never defined at all (every Angular component host), so nothing here may
|
|
103
|
+
* keep a boundary alive: one `whenDefined` per name for the page, closing over only the waiting
|
|
104
|
+
* set, and each element's restamper held weakly by the element.
|
|
105
|
+
*/
|
|
106
|
+
const awaitingDefinition = new Map();
|
|
107
|
+
const restampers = new WeakMap();
|
|
108
|
+
function restampWhenDefined(element, restamp) {
|
|
109
|
+
const alreadyWaiting = restampers.has(element);
|
|
110
|
+
restampers.set(element, restamp);
|
|
111
|
+
if (alreadyWaiting)
|
|
112
|
+
return;
|
|
113
|
+
const name = element.localName;
|
|
114
|
+
let waiting = awaitingDefinition.get(name);
|
|
115
|
+
if (!waiting) {
|
|
116
|
+
const entry = { elements: new Set(), pruneAt: 1024 };
|
|
117
|
+
awaitingDefinition.set(name, (waiting = entry));
|
|
118
|
+
customElements.whenDefined(name).then(() => {
|
|
119
|
+
awaitingDefinition.delete(name);
|
|
120
|
+
for (const ref of entry.elements) {
|
|
121
|
+
const defined = ref.deref();
|
|
122
|
+
if (defined)
|
|
123
|
+
restampers.get(defined)?.(defined);
|
|
124
|
+
}
|
|
125
|
+
}, () => awaitingDefinition.delete(name));
|
|
126
|
+
}
|
|
127
|
+
// a long-lived list of a never-defined element leaves dead refs behind; sweep them as it grows
|
|
128
|
+
if (waiting.elements.size >= waiting.pruneAt) {
|
|
129
|
+
for (const ref of waiting.elements)
|
|
130
|
+
if (!ref.deref())
|
|
131
|
+
waiting.elements.delete(ref);
|
|
132
|
+
waiting.pruneAt = Math.max(1024, waiting.elements.size * 2);
|
|
133
|
+
}
|
|
134
|
+
waiting.elements.add(new WeakRef(element));
|
|
135
|
+
}
|
|
51
136
|
export function createFragmentBoundary(options) {
|
|
52
137
|
const { realmDocument, shadowRoot, braidDocumentElement, abortSignal, htmlPolicy } = options;
|
|
53
138
|
const stampedNodes = new WeakSet();
|
|
54
139
|
const stampedProtos = new Map();
|
|
55
140
|
const stampedProtoSet = new WeakSet();
|
|
56
141
|
let warnedAboutUnstampedEntry = false;
|
|
142
|
+
/** Restamps an upgraded element, unless it has left this fragment or the fragment is gone. */
|
|
143
|
+
function restampIfStillInFragment(element) {
|
|
144
|
+
if (!abortSignal.aborted && isInFragmentDom(element))
|
|
145
|
+
stampNode(element);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Stamps what an insertion put in place: anything in the fragment's DOM, or in a detached tree,
|
|
149
|
+
* which may enter it later. Not a node a component's own insertion override placed in the host's
|
|
150
|
+
* DOM instead (a portal, an overlay) — that is the host's, and stays on pristine prototypes.
|
|
151
|
+
*/
|
|
152
|
+
function stampInserted(node) {
|
|
153
|
+
if (node.isConnected && !isInFragmentDom(node))
|
|
154
|
+
return;
|
|
155
|
+
stampTree(node);
|
|
156
|
+
}
|
|
57
157
|
/** A node is inside this fragment's DOM iff its plain root is the fragment's shadow root. */
|
|
58
158
|
function isInFragmentDom(node) {
|
|
59
159
|
return native.getRootNode.call(node) === shadowRoot;
|
|
@@ -174,13 +274,13 @@ export function createFragmentBoundary(options) {
|
|
|
174
274
|
* `compatDomInsertion`). In all cases the inserted subtree is stamped so the stamped set
|
|
175
275
|
* stays closed.
|
|
176
276
|
*/
|
|
177
|
-
function makeSingleNodeInsertion(nativeFn) {
|
|
277
|
+
function makeSingleNodeInsertion(name, nativeFn) {
|
|
178
278
|
return function boundaryInsertion(childNode) {
|
|
179
279
|
const args = arguments;
|
|
180
280
|
const futureStamps = nodesToStampAfterInsertion(childNode);
|
|
181
281
|
const doInsertTheNode = () => {
|
|
182
|
-
const result = nativeFn.apply(this, args);
|
|
183
|
-
futureStamps.forEach(
|
|
282
|
+
const result = unstampedMethod(this, name, nativeFn).apply(this, args);
|
|
283
|
+
futureStamps.forEach(stampInserted);
|
|
184
284
|
return result;
|
|
185
285
|
};
|
|
186
286
|
if (!isInFragmentDom(this)) {
|
|
@@ -195,8 +295,8 @@ export function createFragmentBoundary(options) {
|
|
|
195
295
|
const args = arguments;
|
|
196
296
|
const futureStamps = nodesToStampAfterInsertion(newChild);
|
|
197
297
|
const doInsertTheNode = () => {
|
|
198
|
-
const result = native.replaceChild.apply(this, args);
|
|
199
|
-
futureStamps.forEach(
|
|
298
|
+
const result = unstampedMethod(this, 'replaceChild', native.replaceChild).apply(this, args);
|
|
299
|
+
futureStamps.forEach(stampInserted);
|
|
200
300
|
return result;
|
|
201
301
|
};
|
|
202
302
|
if (!isInFragmentDom(this)) {
|
|
@@ -210,8 +310,8 @@ export function createFragmentBoundary(options) {
|
|
|
210
310
|
function boundaryInsertAdjacentElement(where, element) {
|
|
211
311
|
const args = arguments;
|
|
212
312
|
const doInsertTheNode = () => {
|
|
213
|
-
const result = native.insertAdjacentElement.apply(this, args);
|
|
214
|
-
|
|
313
|
+
const result = unstampedMethod(this, 'insertAdjacentElement', native.insertAdjacentElement).apply(this, args);
|
|
314
|
+
stampInserted(element);
|
|
215
315
|
return result;
|
|
216
316
|
};
|
|
217
317
|
if (!isInFragmentDom(this)) {
|
|
@@ -224,14 +324,14 @@ export function createFragmentBoundary(options) {
|
|
|
224
324
|
* replaceWith): the native insertion runs once, after every node argument has been
|
|
225
325
|
* preprocessed.
|
|
226
326
|
*/
|
|
227
|
-
function makeMultiNodeInsertion(nativeFn) {
|
|
327
|
+
function makeMultiNodeInsertion(name, nativeFn) {
|
|
228
328
|
return function boundaryMultiInsertion(...nodes) {
|
|
229
329
|
const args = arguments;
|
|
230
330
|
const nodeArgs = nodes.filter((node) => typeof node !== 'string');
|
|
231
331
|
const futureStamps = nodeArgs.flatMap(nodesToStampAfterInsertion);
|
|
232
332
|
const doInsertTheNodes = () => {
|
|
233
|
-
nativeFn.apply(this, args);
|
|
234
|
-
futureStamps.forEach(
|
|
333
|
+
unstampedMethod(this, name, nativeFn).apply(this, args);
|
|
334
|
+
futureStamps.forEach(stampInserted);
|
|
235
335
|
};
|
|
236
336
|
if (!isInFragmentDom(this)) {
|
|
237
337
|
return doInsertTheNodes();
|
|
@@ -272,18 +372,18 @@ export function createFragmentBoundary(options) {
|
|
|
272
372
|
};
|
|
273
373
|
}
|
|
274
374
|
const elementOverrides = {
|
|
275
|
-
appendChild: { configurable: true, writable: true, value: makeSingleNodeInsertion(native.appendChild) },
|
|
276
|
-
insertBefore: { configurable: true, writable: true, value: makeSingleNodeInsertion(native.insertBefore) },
|
|
375
|
+
appendChild: { configurable: true, writable: true, value: makeSingleNodeInsertion('appendChild', native.appendChild) },
|
|
376
|
+
insertBefore: { configurable: true, writable: true, value: makeSingleNodeInsertion('insertBefore', native.insertBefore) },
|
|
277
377
|
replaceChild: { configurable: true, writable: true, value: boundaryReplaceChild },
|
|
278
378
|
insertAdjacentElement: { configurable: true, writable: true, value: boundaryInsertAdjacentElement },
|
|
279
|
-
append: { configurable: true, writable: true, value: makeMultiNodeInsertion(native.elementAppend) },
|
|
280
|
-
prepend: { configurable: true, writable: true, value: makeMultiNodeInsertion(native.elementPrepend) },
|
|
379
|
+
append: { configurable: true, writable: true, value: makeMultiNodeInsertion('append', native.elementAppend) },
|
|
380
|
+
prepend: { configurable: true, writable: true, value: makeMultiNodeInsertion('prepend', native.elementPrepend) },
|
|
281
381
|
replaceChildren: {
|
|
282
382
|
configurable: true,
|
|
283
383
|
writable: true,
|
|
284
|
-
value: makeMultiNodeInsertion(native.elementReplaceChildren),
|
|
384
|
+
value: makeMultiNodeInsertion('replaceChildren', native.elementReplaceChildren),
|
|
285
385
|
},
|
|
286
|
-
replaceWith: { configurable: true, writable: true, value: makeMultiNodeInsertion(native.elementReplaceWith) },
|
|
386
|
+
replaceWith: { configurable: true, writable: true, value: makeMultiNodeInsertion('replaceWith', native.elementReplaceWith) },
|
|
287
387
|
insertAdjacentHTML: {
|
|
288
388
|
configurable: true,
|
|
289
389
|
writable: true,
|
|
@@ -314,7 +414,7 @@ export function createFragmentBoundary(options) {
|
|
|
314
414
|
}
|
|
315
415
|
else {
|
|
316
416
|
// also what throws for an unknown position, or outside an element with no parent
|
|
317
|
-
native.insertAdjacentHTML.call(this, position, text);
|
|
417
|
+
unstampedMethod(this, 'insertAdjacentHTML', native.insertAdjacentHTML).call(this, position, text);
|
|
318
418
|
}
|
|
319
419
|
stampTree(inside ? this : this.parentNode ?? this);
|
|
320
420
|
},
|
|
@@ -323,7 +423,7 @@ export function createFragmentBoundary(options) {
|
|
|
323
423
|
configurable: true,
|
|
324
424
|
writable: true,
|
|
325
425
|
value: function boundaryCloneNode(deep) {
|
|
326
|
-
const clone = native.cloneNode.call(this, deep);
|
|
426
|
+
const clone = unstampedMethod(this, 'cloneNode', native.cloneNode).call(this, deep);
|
|
327
427
|
inheritProvenance(this, clone, Boolean(deep));
|
|
328
428
|
stampTree(clone);
|
|
329
429
|
return clone;
|
|
@@ -350,6 +450,10 @@ export function createFragmentBoundary(options) {
|
|
|
350
450
|
},
|
|
351
451
|
...makeHtmlParsingOverrides(native.elementInnerHTML),
|
|
352
452
|
};
|
|
453
|
+
for (const descriptor of Object.values(elementOverrides)) {
|
|
454
|
+
if (typeof descriptor.value === 'function')
|
|
455
|
+
allBoundaryWrappers.add(descriptor.value);
|
|
456
|
+
}
|
|
353
457
|
/**
|
|
354
458
|
* What a URL set from fragment script resolves against, read when it is set: the namespace from
|
|
355
459
|
* the realm's base, and the fragment's own `<base href>` from its markup — the two the gateway
|
|
@@ -362,13 +466,16 @@ export function createFragmentBoundary(options) {
|
|
|
362
466
|
function stampNode(node) {
|
|
363
467
|
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
364
468
|
return;
|
|
365
|
-
|
|
366
|
-
|
|
469
|
+
// by prototype, not by membership: an upgrade replaces a stamped element's prototype, and the
|
|
470
|
+
// next stamping pass that reaches it should put the stamp back
|
|
367
471
|
const proto = Object.getPrototypeOf(node);
|
|
368
472
|
if (proto === null || stampedProtoSet.has(proto)) {
|
|
369
473
|
stampedNodes.add(node);
|
|
370
474
|
return;
|
|
371
475
|
}
|
|
476
|
+
if (proto === HTMLElement.prototype && node.localName.includes('-')) {
|
|
477
|
+
restampWhenDefined(node, restampIfStillInFragment);
|
|
478
|
+
}
|
|
372
479
|
let stampedProto = stampedProtos.get(proto);
|
|
373
480
|
if (!stampedProto) {
|
|
374
481
|
// a subresource URL the fragment's code sets resolves into its namespace, as one in its
|
|
@@ -379,6 +486,7 @@ export function createFragmentBoundary(options) {
|
|
|
379
486
|
});
|
|
380
487
|
stampedProtos.set(proto, stampedProto);
|
|
381
488
|
stampedProtoSet.add(stampedProto);
|
|
489
|
+
allStampedProtos.add(stampedProto);
|
|
382
490
|
}
|
|
383
491
|
Object.setPrototypeOf(node, stampedProto);
|
|
384
492
|
stampedNodes.add(node);
|
|
@@ -399,15 +507,15 @@ export function createFragmentBoundary(options) {
|
|
|
399
507
|
* (reporting the main document) — only insertions are intercepted.
|
|
400
508
|
*/
|
|
401
509
|
Object.defineProperties(shadowRoot, {
|
|
402
|
-
appendChild: { configurable: true, writable: true, value: makeSingleNodeInsertion(native.appendChild) },
|
|
403
|
-
insertBefore: { configurable: true, writable: true, value: makeSingleNodeInsertion(native.insertBefore) },
|
|
510
|
+
appendChild: { configurable: true, writable: true, value: makeSingleNodeInsertion('appendChild', native.appendChild) },
|
|
511
|
+
insertBefore: { configurable: true, writable: true, value: makeSingleNodeInsertion('insertBefore', native.insertBefore) },
|
|
404
512
|
replaceChild: { configurable: true, writable: true, value: boundaryReplaceChild },
|
|
405
|
-
append: { configurable: true, writable: true, value: makeMultiNodeInsertion(native.fragmentAppend) },
|
|
406
|
-
prepend: { configurable: true, writable: true, value: makeMultiNodeInsertion(native.fragmentPrepend) },
|
|
513
|
+
append: { configurable: true, writable: true, value: makeMultiNodeInsertion('append', native.fragmentAppend) },
|
|
514
|
+
prepend: { configurable: true, writable: true, value: makeMultiNodeInsertion('prepend', native.fragmentPrepend) },
|
|
407
515
|
replaceChildren: {
|
|
408
516
|
configurable: true,
|
|
409
517
|
writable: true,
|
|
410
|
-
value: makeMultiNodeInsertion(native.fragmentReplaceChildren),
|
|
518
|
+
value: makeMultiNodeInsertion('replaceChildren', native.fragmentReplaceChildren),
|
|
411
519
|
},
|
|
412
520
|
...makeHtmlParsingOverrides(native.shadowRootInnerHTML),
|
|
413
521
|
});
|