@absolutejs/absolute 0.15.0 → 0.15.1

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,288 @@
1
+ /* CSS reload/preload utilities for HMR */
2
+
3
+ import type { CSSUpdateResult } from '../../../types/client';
4
+ import { hmrState } from '../../../types/client';
5
+
6
+ export const getCSSBaseName = (href: string) => {
7
+ const fileName = href.split('?')[0]!.split('/').pop() || '';
8
+ return fileName.split('.')[0]!;
9
+ };
10
+
11
+ export const reloadCSSStylesheets = (manifest: Record<string, string>) => {
12
+ const stylesheets = document.querySelectorAll('link[rel="stylesheet"]');
13
+ stylesheets.forEach(function (link) {
14
+ const href = (link as HTMLLinkElement).getAttribute('href');
15
+ if (!href || href.includes('htmx.min.js')) return;
16
+
17
+ let newHref: string | null = null;
18
+ if (manifest) {
19
+ const baseName = href
20
+ .split('/')
21
+ .pop()!
22
+ .replace(/\.[^.]*$/, '');
23
+ const manifestKey =
24
+ baseName
25
+ .split('-')
26
+ .map(function (part) {
27
+ return part.charAt(0).toUpperCase() + part.slice(1);
28
+ })
29
+ .join('') + 'CSS';
30
+
31
+ if (manifest[manifestKey]) {
32
+ newHref = manifest[manifestKey]!;
33
+ } else {
34
+ for (const [key, value] of Object.entries(manifest)) {
35
+ if (key.endsWith('CSS') && value.includes(baseName)) {
36
+ newHref = value;
37
+ break;
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ if (newHref && newHref !== href) {
44
+ (link as HTMLLinkElement).href = newHref + '?t=' + Date.now();
45
+ } else {
46
+ const url = new URL(href, window.location.origin);
47
+ url.searchParams.set('t', Date.now().toString());
48
+ (link as HTMLLinkElement).href = url.toString();
49
+ }
50
+ });
51
+ };
52
+
53
+ /* Shared CSS preload/swap logic used by HTML and HTMX handlers.
54
+ Returns tracking arrays for coordinating CSS load with body patching. */
55
+ export const processCSSLinks = (headHTML: string) => {
56
+ const tempDiv = document.createElement('div');
57
+ tempDiv.innerHTML = headHTML;
58
+ const newStylesheets = tempDiv.querySelectorAll('link[rel="stylesheet"]');
59
+ const existingStylesheets = Array.from(
60
+ document.head.querySelectorAll<HTMLLinkElement>(
61
+ 'link[rel="stylesheet"]'
62
+ )
63
+ );
64
+
65
+ const newHrefs = Array.from(newStylesheets).map(function (link) {
66
+ const href = link.getAttribute('href') || '';
67
+ return getCSSBaseName(href);
68
+ });
69
+
70
+ const linksToRemove: HTMLLinkElement[] = [];
71
+ const linksToWaitFor: Promise<void>[] = [];
72
+ const linksToActivate: HTMLLinkElement[] = [];
73
+
74
+ newStylesheets.forEach(function (newLink) {
75
+ const href = newLink.getAttribute('href');
76
+ if (!href) return;
77
+
78
+ const baseNew = getCSSBaseName(href);
79
+
80
+ let existingLink: HTMLLinkElement | null = null;
81
+ document.head
82
+ .querySelectorAll('link[rel="stylesheet"]')
83
+ .forEach(function (existing) {
84
+ const existingHref =
85
+ (existing as HTMLLinkElement).getAttribute('href') || '';
86
+ const baseExisting = getCSSBaseName(existingHref);
87
+ if (
88
+ baseExisting === baseNew ||
89
+ baseExisting.includes(baseNew) ||
90
+ baseNew.includes(baseExisting)
91
+ ) {
92
+ existingLink = existing as HTMLLinkElement;
93
+ }
94
+ });
95
+
96
+ if (existingLink) {
97
+ const existingHrefAttr = (
98
+ existingLink as HTMLLinkElement
99
+ ).getAttribute('href');
100
+ const existingHref = existingHrefAttr
101
+ ? existingHrefAttr.split('?')[0]
102
+ : '';
103
+ const newHrefBase = href.split('?')[0];
104
+ if (existingHref !== newHrefBase) {
105
+ const newLinkElement = document.createElement('link');
106
+ newLinkElement.rel = 'stylesheet';
107
+ newLinkElement.media = 'print';
108
+ const newHref =
109
+ href + (href.includes('?') ? '&' : '?') + 't=' + Date.now();
110
+ newLinkElement.href = newHref;
111
+
112
+ linksToRemove.push(existingLink as HTMLLinkElement);
113
+ linksToActivate.push(newLinkElement);
114
+
115
+ const loadPromise = createCSSLoadPromise(
116
+ newLinkElement,
117
+ newHref
118
+ );
119
+ document.head.appendChild(newLinkElement);
120
+ linksToWaitFor.push(loadPromise);
121
+ }
122
+ } else {
123
+ const newLinkElement = document.createElement('link');
124
+ newLinkElement.rel = 'stylesheet';
125
+ newLinkElement.media = 'print';
126
+ const newHref =
127
+ href + (href.includes('?') ? '&' : '?') + 't=' + Date.now();
128
+ newLinkElement.href = newHref;
129
+
130
+ linksToActivate.push(newLinkElement);
131
+
132
+ const loadPromise = createCSSLoadPromise(newLinkElement, newHref);
133
+ document.head.appendChild(newLinkElement);
134
+ linksToWaitFor.push(loadPromise);
135
+ }
136
+ });
137
+
138
+ existingStylesheets.forEach(function (existingLink) {
139
+ const existingHref = existingLink.getAttribute('href') || '';
140
+ const baseExisting = getCSSBaseName(existingHref);
141
+ const stillExists = newHrefs.some(function (newBase) {
142
+ return (
143
+ baseExisting === newBase ||
144
+ baseExisting.includes(newBase) ||
145
+ newBase.includes(baseExisting)
146
+ );
147
+ });
148
+
149
+ if (!stillExists) {
150
+ const wasHandled = Array.from(newStylesheets).some(
151
+ function (newLink) {
152
+ const newHref = newLink.getAttribute('href') || '';
153
+ const baseNewLocal = getCSSBaseName(newHref);
154
+ return (
155
+ baseExisting === baseNewLocal ||
156
+ baseExisting.includes(baseNewLocal) ||
157
+ baseNewLocal.includes(baseExisting)
158
+ );
159
+ }
160
+ );
161
+
162
+ if (!wasHandled) {
163
+ linksToRemove.push(existingLink);
164
+ }
165
+ }
166
+ });
167
+
168
+ return { linksToActivate, linksToRemove, linksToWaitFor };
169
+ };
170
+
171
+ const createCSSLoadPromise = (
172
+ linkElement: HTMLLinkElement,
173
+ newHref: string
174
+ ) => {
175
+ return new Promise<void>(function (resolve) {
176
+ let resolved = false;
177
+ const doResolve = function () {
178
+ if (resolved) return;
179
+ resolved = true;
180
+ resolve();
181
+ };
182
+
183
+ const verifyCSSOM = function () {
184
+ try {
185
+ const sheets = Array.from(document.styleSheets);
186
+ return sheets.some(function (sheet) {
187
+ return (
188
+ sheet.href &&
189
+ sheet.href.includes(newHref.split('?')[0]!)
190
+ );
191
+ });
192
+ } catch {
193
+ return false;
194
+ }
195
+ };
196
+
197
+ linkElement.onload = function () {
198
+ let checkCount = 0;
199
+ const checkCSSOM = function () {
200
+ checkCount++;
201
+ if (verifyCSSOM() || checkCount > 10) {
202
+ doResolve();
203
+ } else {
204
+ requestAnimationFrame(checkCSSOM);
205
+ }
206
+ };
207
+ requestAnimationFrame(checkCSSOM);
208
+ };
209
+
210
+ linkElement.onerror = function () {
211
+ setTimeout(function () {
212
+ doResolve();
213
+ }, 50);
214
+ };
215
+
216
+ setTimeout(function () {
217
+ if (linkElement.sheet && !resolved) {
218
+ doResolve();
219
+ }
220
+ }, 100);
221
+
222
+ setTimeout(function () {
223
+ if (!resolved) {
224
+ doResolve();
225
+ }
226
+ }, 500);
227
+ });
228
+ };
229
+
230
+ /* Coordinate CSS load with body update: waits for CSS, patches body,
231
+ activates new CSS, removes old CSS. Handles first-update delay. */
232
+ export const waitForCSSAndUpdate = (
233
+ cssResult: CSSUpdateResult,
234
+ updateBody: () => void
235
+ ) => {
236
+ const { linksToActivate, linksToRemove, linksToWaitFor } = cssResult;
237
+
238
+ if (linksToWaitFor.length > 0) {
239
+ Promise.all(linksToWaitFor).then(function () {
240
+ setTimeout(function () {
241
+ requestAnimationFrame(function () {
242
+ requestAnimationFrame(function () {
243
+ requestAnimationFrame(function () {
244
+ updateBody();
245
+ linksToActivate.forEach(function (link) {
246
+ link.media = 'all';
247
+ });
248
+ requestAnimationFrame(function () {
249
+ linksToRemove.forEach(function (link) {
250
+ if (link.parentNode) {
251
+ link.remove();
252
+ }
253
+ });
254
+ if (hmrState.isFirstHMRUpdate) {
255
+ hmrState.isFirstHMRUpdate = false;
256
+ }
257
+ });
258
+ });
259
+ });
260
+ });
261
+ }, 50);
262
+ });
263
+ } else {
264
+ const doUpdate = function () {
265
+ requestAnimationFrame(function () {
266
+ requestAnimationFrame(function () {
267
+ requestAnimationFrame(function () {
268
+ updateBody();
269
+ requestAnimationFrame(function () {
270
+ linksToRemove.forEach(function (link) {
271
+ if (link.parentNode) {
272
+ link.remove();
273
+ }
274
+ });
275
+ });
276
+ });
277
+ });
278
+ });
279
+ };
280
+
281
+ if (hmrState.isFirstHMRUpdate) {
282
+ hmrState.isFirstHMRUpdate = false;
283
+ setTimeout(doUpdate, 50);
284
+ } else {
285
+ doUpdate();
286
+ }
287
+ }
288
+ };
@@ -0,0 +1,261 @@
1
+ /* DOM diffing/patching for in-place updates (zero flicker) */
2
+
3
+ const getElementKey = (el: Node, index: number) => {
4
+ if (el.nodeType !== Node.ELEMENT_NODE) return 'text_' + index;
5
+ const element = el as Element;
6
+ if (element.id) return 'id_' + element.id;
7
+ if (element.hasAttribute('data-key'))
8
+ return 'key_' + element.getAttribute('data-key');
9
+ return 'tag_' + element.tagName + '_' + index;
10
+ };
11
+
12
+ const updateElementAttributes = (oldEl: Element, newEl: Element) => {
13
+ const newAttrs = Array.from(newEl.attributes);
14
+ const oldAttrs = Array.from(oldEl.attributes);
15
+ const runtimeAttrs = ['data-hmr-listeners-attached'];
16
+
17
+ oldAttrs.forEach(function (oldAttr) {
18
+ if (
19
+ !newEl.hasAttribute(oldAttr.name) &&
20
+ runtimeAttrs.indexOf(oldAttr.name) === -1
21
+ ) {
22
+ oldEl.removeAttribute(oldAttr.name);
23
+ }
24
+ });
25
+
26
+ newAttrs.forEach(function (newAttr) {
27
+ if (
28
+ runtimeAttrs.indexOf(newAttr.name) !== -1 &&
29
+ oldEl.hasAttribute(newAttr.name)
30
+ ) {
31
+ return;
32
+ }
33
+ const oldValue = oldEl.getAttribute(newAttr.name);
34
+ if (oldValue !== newAttr.value) {
35
+ oldEl.setAttribute(newAttr.name, newAttr.value);
36
+ }
37
+ });
38
+ };
39
+
40
+ const updateTextNode = (oldNode: Node, newNode: Node) => {
41
+ if (oldNode.nodeValue !== newNode.nodeValue) {
42
+ oldNode.nodeValue = newNode.nodeValue;
43
+ }
44
+ };
45
+
46
+ interface KeyedEntry {
47
+ index: number;
48
+ node: Node;
49
+ }
50
+
51
+ const matchChildren = (oldChildren: Node[], newChildren: Node[]) => {
52
+ const oldMap = new Map<string, KeyedEntry[]>();
53
+ const newMap = new Map<string, KeyedEntry[]>();
54
+
55
+ oldChildren.forEach(function (child, idx) {
56
+ const key = getElementKey(child, idx);
57
+ if (!oldMap.has(key)) {
58
+ oldMap.set(key, []);
59
+ }
60
+ oldMap.get(key)!.push({ index: idx, node: child });
61
+ });
62
+
63
+ newChildren.forEach(function (child, idx) {
64
+ const key = getElementKey(child, idx);
65
+ if (!newMap.has(key)) {
66
+ newMap.set(key, []);
67
+ }
68
+ newMap.get(key)!.push({ index: idx, node: child });
69
+ });
70
+
71
+ return { newMap, oldMap };
72
+ };
73
+
74
+ const isHMRScript = (el: Node) => {
75
+ return (
76
+ el.nodeType === Node.ELEMENT_NODE &&
77
+ (el as Element).hasAttribute &&
78
+ (el as Element).hasAttribute('data-hmr-client')
79
+ );
80
+ };
81
+
82
+ const isHMRPreserved = (el: Node) => {
83
+ return (
84
+ isHMRScript(el) ||
85
+ (el.nodeType === Node.ELEMENT_NODE &&
86
+ (el as Element).hasAttribute &&
87
+ (el as Element).hasAttribute('data-hmr-overlay'))
88
+ );
89
+ };
90
+
91
+ const patchNode = (oldNode: Node, newNode: Node) => {
92
+ if (
93
+ oldNode.nodeType === Node.TEXT_NODE &&
94
+ newNode.nodeType === Node.TEXT_NODE
95
+ ) {
96
+ updateTextNode(oldNode, newNode);
97
+ return;
98
+ }
99
+
100
+ if (
101
+ oldNode.nodeType === Node.ELEMENT_NODE &&
102
+ newNode.nodeType === Node.ELEMENT_NODE
103
+ ) {
104
+ const oldEl = oldNode as Element;
105
+ const newEl = newNode as Element;
106
+
107
+ if (oldEl.tagName !== newEl.tagName) {
108
+ const clone = newEl.cloneNode(true);
109
+ oldEl.replaceWith(clone);
110
+ return;
111
+ }
112
+
113
+ updateElementAttributes(oldEl, newEl);
114
+
115
+ const oldChildren = Array.from(oldNode.childNodes);
116
+ const newChildren = Array.from(newNode.childNodes);
117
+
118
+ const oldChildrenFiltered = oldChildren.filter(function (child) {
119
+ return (
120
+ !isHMRScript(child) &&
121
+ !(
122
+ child.nodeType === Node.ELEMENT_NODE &&
123
+ (child as Element).tagName === 'SCRIPT'
124
+ )
125
+ );
126
+ });
127
+ const newChildrenFiltered = newChildren.filter(function (child) {
128
+ return (
129
+ !isHMRScript(child) &&
130
+ !(
131
+ child.nodeType === Node.ELEMENT_NODE &&
132
+ (child as Element).tagName === 'SCRIPT'
133
+ )
134
+ );
135
+ });
136
+
137
+ const { oldMap } = matchChildren(
138
+ oldChildrenFiltered,
139
+ newChildrenFiltered
140
+ );
141
+ const matchedOld = new Set<Node>();
142
+
143
+ newChildrenFiltered.forEach(function (newChild, newIndex) {
144
+ const newKey = getElementKey(newChild, newIndex);
145
+ const oldMatches = oldMap.get(newKey) || [];
146
+
147
+ if (oldMatches.length > 0) {
148
+ let bestMatch: KeyedEntry | null = null;
149
+ for (let idx = 0; idx < oldMatches.length; idx++) {
150
+ if (!matchedOld.has(oldMatches[idx]!.node)) {
151
+ bestMatch = oldMatches[idx]!;
152
+ break;
153
+ }
154
+ }
155
+ if (!bestMatch && oldMatches.length > 0) {
156
+ bestMatch = oldMatches[0]!;
157
+ }
158
+ if (bestMatch && !matchedOld.has(bestMatch.node)) {
159
+ matchedOld.add(bestMatch.node);
160
+ patchNode(bestMatch.node, newChild);
161
+ } else if (oldMatches.length > 0) {
162
+ const clone = newChild.cloneNode(true);
163
+ oldNode.insertBefore(
164
+ clone,
165
+ oldChildrenFiltered[newIndex] || null
166
+ );
167
+ }
168
+ } else {
169
+ const clone = newChild.cloneNode(true);
170
+ oldNode.insertBefore(
171
+ clone,
172
+ oldChildrenFiltered[newIndex] || null
173
+ );
174
+ }
175
+ });
176
+
177
+ oldChildrenFiltered.forEach(function (oldChild) {
178
+ if (!matchedOld.has(oldChild) && !isHMRPreserved(oldChild)) {
179
+ oldChild.remove();
180
+ }
181
+ });
182
+ }
183
+ };
184
+
185
+ export const patchDOMInPlace = (oldContainer: HTMLElement, newHTML: string) => {
186
+ const tempDiv = document.createElement('div');
187
+ tempDiv.innerHTML = newHTML;
188
+ const newContainer = tempDiv;
189
+
190
+ const oldChildren = Array.from(oldContainer.childNodes);
191
+ const newChildren = Array.from(newContainer.childNodes);
192
+
193
+ const oldChildrenFiltered = oldChildren.filter(function (child) {
194
+ return !(
195
+ child.nodeType === Node.ELEMENT_NODE &&
196
+ (child as Element).tagName === 'SCRIPT' &&
197
+ !(child as Element).hasAttribute('data-hmr-client')
198
+ );
199
+ });
200
+ const newChildrenFiltered = newChildren.filter(function (child) {
201
+ return !(
202
+ child.nodeType === Node.ELEMENT_NODE &&
203
+ (child as Element).tagName === 'SCRIPT'
204
+ );
205
+ });
206
+
207
+ const { oldMap } = matchChildren(oldChildrenFiltered, newChildrenFiltered);
208
+ const matchedOld = new Set<Node>();
209
+
210
+ newChildrenFiltered.forEach(function (newChild, newIndex) {
211
+ const newKey = getElementKey(newChild, newIndex);
212
+ const oldMatches = oldMap.get(newKey) || [];
213
+
214
+ if (oldMatches.length > 0) {
215
+ let bestMatch: KeyedEntry | null = null;
216
+ for (let idx = 0; idx < oldMatches.length; idx++) {
217
+ if (!matchedOld.has(oldMatches[idx]!.node)) {
218
+ bestMatch = oldMatches[idx]!;
219
+ break;
220
+ }
221
+ }
222
+ if (!bestMatch && oldMatches.length > 0) {
223
+ bestMatch = oldMatches[0]!;
224
+ }
225
+ if (bestMatch && !matchedOld.has(bestMatch.node)) {
226
+ matchedOld.add(bestMatch.node);
227
+ patchNode(bestMatch.node, newChild);
228
+ } else {
229
+ const clone = newChild.cloneNode(true);
230
+ oldContainer.insertBefore(
231
+ clone,
232
+ oldChildrenFiltered[newIndex] || null
233
+ );
234
+ }
235
+ } else {
236
+ const clone = newChild.cloneNode(true);
237
+ oldContainer.insertBefore(
238
+ clone,
239
+ oldChildrenFiltered[newIndex] || null
240
+ );
241
+ }
242
+ });
243
+
244
+ oldChildrenFiltered.forEach(function (oldChild) {
245
+ if (
246
+ !matchedOld.has(oldChild) &&
247
+ !(
248
+ oldChild.nodeType === Node.ELEMENT_NODE &&
249
+ (oldChild as Element).tagName === 'SCRIPT' &&
250
+ (oldChild as Element).hasAttribute('data-hmr-client')
251
+ ) &&
252
+ !(
253
+ oldChild.nodeType === Node.ELEMENT_NODE &&
254
+ (oldChild as Element).hasAttribute &&
255
+ (oldChild as Element).hasAttribute('data-hmr-overlay')
256
+ )
257
+ ) {
258
+ oldChild.remove();
259
+ }
260
+ });
261
+ };