@jxsuite/import 0.40.8 → 0.40.10
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 +6 -4
- package/src/ai-componentize.ts +49 -71
- package/src/apply-accordions.ts +32 -25
- package/src/apply-disclosures.ts +30 -40
- package/src/apply-popovers.ts +24 -29
- package/src/apply-styles.ts +6 -5
- package/src/asset-collect.ts +17 -20
- package/src/asset-download.ts +3 -3
- package/src/asset-rewrite.ts +28 -28
- package/src/breakpoint-plan.ts +7 -2
- package/src/capture.ts +11 -5
- package/src/cli.ts +4 -3
- package/src/componentize.ts +32 -37
- package/src/crawl.ts +2 -2
- package/src/css-tokens.ts +2 -1
- package/src/derived-geometry.ts +13 -14
- package/src/emit.ts +37 -22
- package/src/layout-detect.ts +10 -11
- package/src/pipeline.ts +1 -1
- package/src/run.ts +23 -19
- package/src/strip-classes.ts +6 -5
- package/src/style-capture.ts +5 -0
- package/src/style-diff.ts +1 -4
- package/src/to-jx.ts +13 -9
- package/src/toggle-algebra.ts +7 -4
- package/src/verify.ts +53 -40
package/src/apply-popovers.ts
CHANGED
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
* ignored it would produce a popover that opens into nothing.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import
|
|
29
|
+
import { getNestedStyle, isString } from "@jxsuite/schema/guards";
|
|
30
|
+
import type { JxElement, JxStyle } from "@jxsuite/schema/types";
|
|
30
31
|
import { POPOVER_DEFAULT_MODE, POPOVER_INVOKER_TAGS } from "@jxsuite/schema/overlays";
|
|
31
32
|
|
|
32
33
|
/** Attributes naming the panel an invoker controls, in the order they are trusted. */
|
|
@@ -41,22 +42,14 @@ const CONCEALING = new Set(["display", "visibility", "opacity"]);
|
|
|
41
42
|
/** A concealed panel is only a popover if it is also lifted out of the flow. */
|
|
42
43
|
const POSITIONED = new Set(["absolute", "fixed"]);
|
|
43
44
|
|
|
44
|
-
function styleOf(node: JxElement): Record<string, unknown> | undefined {
|
|
45
|
-
return node.style as Record<string, unknown> | undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function attributesOf(node: JxElement): Record<string, unknown> | undefined {
|
|
49
|
-
return node.attributes as Record<string, unknown> | undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
45
|
function childrenOf(node: JxElement): JxElement[] {
|
|
53
46
|
return Array.isArray(node.children)
|
|
54
|
-
?
|
|
47
|
+
? node.children.filter((child): child is JxElement => !isString(child))
|
|
55
48
|
: [];
|
|
56
49
|
}
|
|
57
50
|
|
|
58
51
|
/** Whether a style hides the node outright, and by which declaration. */
|
|
59
|
-
function concealedBy(style:
|
|
52
|
+
function concealedBy(style: JxStyle | undefined): string[] {
|
|
60
53
|
if (!style) {
|
|
61
54
|
return [];
|
|
62
55
|
}
|
|
@@ -84,11 +77,11 @@ function concealedBy(style: Record<string, unknown> | undefined): string[] {
|
|
|
84
77
|
* explicit statements of the same intent.
|
|
85
78
|
*/
|
|
86
79
|
export function looksLikePanel(node: JxElement): boolean {
|
|
87
|
-
const style =
|
|
80
|
+
const { style } = node;
|
|
88
81
|
if (concealedBy(style).length === 0) {
|
|
89
82
|
return false;
|
|
90
83
|
}
|
|
91
|
-
const attributes =
|
|
84
|
+
const { attributes } = node;
|
|
92
85
|
if (attributes?.["role"] === "dialog" || attributes?.["inert"] !== undefined) {
|
|
93
86
|
return true;
|
|
94
87
|
}
|
|
@@ -97,13 +90,13 @@ export function looksLikePanel(node: JxElement): boolean {
|
|
|
97
90
|
|
|
98
91
|
/** The id an invoker names, from whichever attribute carries it. */
|
|
99
92
|
export function targetIdOf(node: JxElement): string | null {
|
|
100
|
-
const attributes =
|
|
93
|
+
const { attributes } = node;
|
|
101
94
|
if (!attributes) {
|
|
102
95
|
return null;
|
|
103
96
|
}
|
|
104
97
|
for (const name of TARGET_ATTRIBUTES) {
|
|
105
98
|
const raw = attributes[name];
|
|
106
|
-
if (
|
|
99
|
+
if (!isString(raw) || raw.length === 0) {
|
|
107
100
|
continue;
|
|
108
101
|
}
|
|
109
102
|
const id = raw.startsWith("#") ? raw.slice(1) : name === "href" ? "" : raw;
|
|
@@ -120,7 +113,7 @@ function looksLikeInvoker(node: JxElement): boolean {
|
|
|
120
113
|
if (tag !== "button" && tag !== "input" && tag !== "a") {
|
|
121
114
|
return false;
|
|
122
115
|
}
|
|
123
|
-
const attributes =
|
|
116
|
+
const { attributes } = node;
|
|
124
117
|
if (!attributes) {
|
|
125
118
|
return false;
|
|
126
119
|
}
|
|
@@ -133,8 +126,8 @@ function looksLikeInvoker(node: JxElement): boolean {
|
|
|
133
126
|
function indexById(root: JxElement): Map<string, JxElement> {
|
|
134
127
|
const index = new Map<string, JxElement>();
|
|
135
128
|
const visit = (node: JxElement): void => {
|
|
136
|
-
const id =
|
|
137
|
-
if (
|
|
129
|
+
const id = node.attributes?.["id"];
|
|
130
|
+
if (isString(id) && !index.has(id)) {
|
|
138
131
|
index.set(id, node);
|
|
139
132
|
}
|
|
140
133
|
for (const child of childrenOf(node)) {
|
|
@@ -183,7 +176,7 @@ function panelBesideAncestor(ancestry: readonly JxElement[]): JxElement | null {
|
|
|
183
176
|
* writing `block` there would be inventing a fact: the element's own default is the honest answer,
|
|
184
177
|
* so nothing is written and the warning is accepted.
|
|
185
178
|
*/
|
|
186
|
-
function openValueFor(property: string, closed:
|
|
179
|
+
function openValueFor(property: string, closed: JxStyle[string]): string | null {
|
|
187
180
|
if (property === "opacity" && (closed === 0 || closed === "0")) {
|
|
188
181
|
return "1";
|
|
189
182
|
}
|
|
@@ -197,12 +190,12 @@ function openValueFor(property: string, closed: unknown): string | null {
|
|
|
197
190
|
* Move a panel's closed-state declarations into `:popover-open`, where they describe the open one.
|
|
198
191
|
*
|
|
199
192
|
* `node` is always a panel `looksLikePanel` already accepted, and that requires a concealing style
|
|
200
|
-
* declaration to exist (`concealedBy(style).length === 0` rejects it otherwise) — so
|
|
201
|
-
*
|
|
193
|
+
* declaration to exist (`concealedBy(style).length === 0` rejects it otherwise) — so `node.style`
|
|
194
|
+
* is never empty here.
|
|
202
195
|
*/
|
|
203
196
|
function liftConcealment(node: JxElement): void {
|
|
204
|
-
const style =
|
|
205
|
-
const open = (style
|
|
197
|
+
const style = node.style!;
|
|
198
|
+
const open = getNestedStyle(style, ":popover-open") ?? {};
|
|
206
199
|
for (const property of Object.keys(style)) {
|
|
207
200
|
if (!CONCEALING.has(property)) {
|
|
208
201
|
continue;
|
|
@@ -213,12 +206,12 @@ function liftConcealment(node: JxElement): void {
|
|
|
213
206
|
design and is carried across unchanged, because the base rule is no longer where it belongs. */
|
|
214
207
|
const openValue = isConcealment ? openValueFor(property, value) : value;
|
|
215
208
|
if (openValue !== null) {
|
|
216
|
-
|
|
209
|
+
open[property] = openValue;
|
|
217
210
|
}
|
|
218
211
|
delete style[property];
|
|
219
212
|
}
|
|
220
213
|
if (Object.keys(open).length > 0) {
|
|
221
|
-
|
|
214
|
+
style[":popover-open"] = open;
|
|
222
215
|
}
|
|
223
216
|
}
|
|
224
217
|
|
|
@@ -248,14 +241,15 @@ export function applyPopovers(root: JxElement): PopoverResult {
|
|
|
248
241
|
|
|
249
242
|
if (panel && panel !== node && !claimed.has(panel) && looksLikePanel(panel)) {
|
|
250
243
|
const tag = String(node.tagName ?? "").toLowerCase();
|
|
251
|
-
|
|
244
|
+
node.attributes ??= {};
|
|
245
|
+
const { attributes } = node;
|
|
252
246
|
|
|
253
247
|
/* `popovertarget` exists only on button and input. An anchor that is really a button
|
|
254
248
|
becomes one; an anchor that navigates keeps its job, because a dead link is a worse
|
|
255
249
|
defect than an unconverted menu. */
|
|
256
250
|
if (!POPOVER_INVOKER_TAGS.has(tag)) {
|
|
257
251
|
const { href } = attributes;
|
|
258
|
-
const navigates =
|
|
252
|
+
const navigates = isString(href) && href.length > 0 && href !== "#";
|
|
259
253
|
if (navigates) {
|
|
260
254
|
result.skippedNavigatingLinks += 1;
|
|
261
255
|
for (const child of childrenOf(node)) {
|
|
@@ -268,9 +262,10 @@ export function applyPopovers(root: JxElement): PopoverResult {
|
|
|
268
262
|
delete attributes["href"];
|
|
269
263
|
}
|
|
270
264
|
|
|
271
|
-
|
|
265
|
+
panel.attributes ??= {};
|
|
266
|
+
const { attributes: panelAttributes } = panel;
|
|
272
267
|
let { id: panelId } = panelAttributes;
|
|
273
|
-
if (
|
|
268
|
+
if (!isString(panelId) || panelId.length === 0) {
|
|
274
269
|
generated += 1;
|
|
275
270
|
panelId = `jx-popover-${generated}`;
|
|
276
271
|
panelAttributes["id"] = panelId;
|
package/src/apply-styles.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* same depth-first element-index order as the browser DOM walk in style-capture.ts.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
6
7
|
import type { JxElement, JxStyle } from "@jxsuite/schema/types";
|
|
7
8
|
import type { DiffedStyle } from "./style-diff.ts";
|
|
8
9
|
import { parseWidthQuery } from "./breakpoint-plan.ts";
|
|
@@ -38,7 +39,7 @@ export function orderBreakpointKeys(
|
|
|
38
39
|
names: readonly string[],
|
|
39
40
|
queries: Record<string, string> | undefined,
|
|
40
41
|
): string[] {
|
|
41
|
-
const rank = (name: string)
|
|
42
|
+
const rank = (name: string) => {
|
|
42
43
|
const parsed = queries?.[name] === undefined ? null : parseWidthQuery(queries[name]!);
|
|
43
44
|
if (!parsed) {
|
|
44
45
|
return { band: 2, width: 0 };
|
|
@@ -83,7 +84,7 @@ export function applyStylesToTree(
|
|
|
83
84
|
|
|
84
85
|
let childIdx = 0;
|
|
85
86
|
for (const child of root.children) {
|
|
86
|
-
if (
|
|
87
|
+
if (isString(child)) {
|
|
87
88
|
continue;
|
|
88
89
|
}
|
|
89
90
|
walkAndApply(child, [childIdx], baseMap, mediaMaps);
|
|
@@ -115,12 +116,12 @@ function walkAndApply(
|
|
|
115
116
|
const delta = deltaMap.get(key);
|
|
116
117
|
if (delta) {
|
|
117
118
|
const filtered = { ...delta };
|
|
118
|
-
dropDerivedGeometry(node, filtered,
|
|
119
|
+
dropDerivedGeometry(node, filtered, node.style ?? {});
|
|
119
120
|
if (Object.keys(filtered).length > 0) {
|
|
120
121
|
if (!node.style) {
|
|
121
122
|
node.style = {};
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
+
node.style[`@${bpName}`] = filtered;
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
}
|
|
@@ -132,7 +133,7 @@ function walkAndApply(
|
|
|
132
133
|
|
|
133
134
|
let childIdx = 0;
|
|
134
135
|
for (const child of node.children) {
|
|
135
|
-
if (
|
|
136
|
+
if (isString(child)) {
|
|
136
137
|
continue;
|
|
137
138
|
}
|
|
138
139
|
walkAndApply(child, [...path, childIdx], baseMap, mediaMaps);
|
package/src/asset-collect.ts
CHANGED
|
@@ -53,11 +53,11 @@ export async function collectAssets(page: ImportPage): Promise<AssetCollectionRe
|
|
|
53
53
|
*/
|
|
54
54
|
const result = await page.evaluate((srcsetSeparator: string) => {
|
|
55
55
|
const separator = new RegExp(srcsetSeparator);
|
|
56
|
-
const assets:
|
|
56
|
+
const assets: DiscoveredAsset[] = [];
|
|
57
57
|
const seen = new Set<string>();
|
|
58
58
|
let inlineSvgCount = 0;
|
|
59
59
|
|
|
60
|
-
function add(url: string, source:
|
|
60
|
+
function add(url: string, source: DiscoveredAsset["source"]) {
|
|
61
61
|
if (!url || seen.has(url)) {
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
@@ -98,8 +98,10 @@ export async function collectAssets(page: ImportPage): Promise<AssetCollectionRe
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
// Img[src]
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
/* Each selector below names its element by tag, so the type argument states what the
|
|
102
|
+
selector already guarantees. */
|
|
103
|
+
for (const el of document.querySelectorAll<HTMLImageElement>("img[src]")) {
|
|
104
|
+
add(el.src, "img-src");
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
// Img[srcset] and source[srcset]
|
|
@@ -112,13 +114,13 @@ export async function collectAssets(page: ImportPage): Promise<AssetCollectionRe
|
|
|
112
114
|
}
|
|
113
115
|
|
|
114
116
|
// Picture > source (type-based, not just srcset)
|
|
115
|
-
for (const el of document.querySelectorAll("picture > source[src]")) {
|
|
116
|
-
add(
|
|
117
|
+
for (const el of document.querySelectorAll<HTMLSourceElement>("picture > source[src]")) {
|
|
118
|
+
add(el.src, "picture-source");
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
// Video[poster]
|
|
120
|
-
for (const el of document.querySelectorAll("video[poster]")) {
|
|
121
|
-
add(
|
|
122
|
+
for (const el of document.querySelectorAll<HTMLVideoElement>("video[poster]")) {
|
|
123
|
+
add(el.poster, "video-poster");
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
// Inline style url() on any element
|
|
@@ -141,8 +143,7 @@ export async function collectAssets(page: ImportPage): Promise<AssetCollectionRe
|
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
// @font-face from stylesheets + retain full source CSS (F0)
|
|
144
|
-
const stylesheets:
|
|
145
|
-
[];
|
|
146
|
+
const stylesheets: CapturedStylesheet[] = [];
|
|
146
147
|
for (const sheet of document.styleSheets) {
|
|
147
148
|
let rules: CSSRuleList | null = null;
|
|
148
149
|
let fullCssText: string | null = null;
|
|
@@ -174,18 +175,18 @@ export async function collectAssets(page: ImportPage): Promise<AssetCollectionRe
|
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
// Favicon and other link icons
|
|
177
|
-
for (const el of document.querySelectorAll(
|
|
178
|
+
for (const el of document.querySelectorAll<HTMLLinkElement>(
|
|
178
179
|
'link[rel~="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]',
|
|
179
180
|
)) {
|
|
180
|
-
const { href } = el
|
|
181
|
+
const { href } = el;
|
|
181
182
|
if (href) {
|
|
182
183
|
add(href, "favicon");
|
|
183
184
|
}
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
// Open Graph images
|
|
187
|
-
for (const el of document.querySelectorAll('meta[property="og:image"]')) {
|
|
188
|
-
const { content } = el
|
|
188
|
+
for (const el of document.querySelectorAll<HTMLMetaElement>('meta[property="og:image"]')) {
|
|
189
|
+
const { content } = el;
|
|
189
190
|
if (content) {
|
|
190
191
|
add(content, "og-image");
|
|
191
192
|
}
|
|
@@ -194,11 +195,7 @@ export async function collectAssets(page: ImportPage): Promise<AssetCollectionRe
|
|
|
194
195
|
// Count inline SVGs (kept inline, not downloaded)
|
|
195
196
|
inlineSvgCount = document.querySelectorAll("svg").length;
|
|
196
197
|
|
|
197
|
-
return { assets, inlineSvgCount, stylesheets }
|
|
198
|
-
assets: { url: string; source: string }[];
|
|
199
|
-
inlineSvgCount: number;
|
|
200
|
-
stylesheets: { href: string | null; cssText: string | null; fontFaceRules: string[] }[];
|
|
201
|
-
};
|
|
198
|
+
return { assets, inlineSvgCount, stylesheets };
|
|
202
199
|
}, SRCSET_SEPARATOR.source);
|
|
203
200
|
|
|
204
201
|
// R2: For cross-origin sheets we couldn't access (cssText=null, has href), fetch them
|
|
@@ -256,5 +253,5 @@ export async function collectAssets(page: ImportPage): Promise<AssetCollectionRe
|
|
|
256
253
|
}
|
|
257
254
|
}
|
|
258
255
|
|
|
259
|
-
return result
|
|
256
|
+
return result;
|
|
260
257
|
}
|
package/src/asset-download.ts
CHANGED
|
@@ -179,12 +179,12 @@ export async function downloadAssets(
|
|
|
179
179
|
const relativePath = `/assets/${subdir}/${filename}`;
|
|
180
180
|
|
|
181
181
|
try {
|
|
182
|
-
const headers
|
|
182
|
+
const headers = new Headers({
|
|
183
183
|
"User-Agent":
|
|
184
184
|
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
185
|
-
};
|
|
185
|
+
});
|
|
186
186
|
if (sourceUrl) {
|
|
187
|
-
headers
|
|
187
|
+
headers.set("Referer", sourceUrl);
|
|
188
188
|
}
|
|
189
189
|
const response = await fetch(url, { headers, redirect: "follow" });
|
|
190
190
|
if (!response.ok) {
|
package/src/asset-rewrite.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* style.backgroundImage (url() references) - $media nested style objects with backgroundImage
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { isNestedStyle, isString } from "@jxsuite/schema/guards";
|
|
8
9
|
import type { JxElement, JxStyle } from "@jxsuite/schema/types";
|
|
9
10
|
import { SRCSET_SEPARATOR } from "./srcset.ts";
|
|
10
11
|
|
|
@@ -52,26 +53,25 @@ export function rewriteAssetUrls(
|
|
|
52
53
|
* the importer would fetch one set of URLs and rewrite another — the attribute would be
|
|
53
54
|
* reassembled around fragments nothing had downloaded.
|
|
54
55
|
*/
|
|
55
|
-
function rewriteSrcset(srcset: string)
|
|
56
|
+
function rewriteSrcset(srcset: string) {
|
|
56
57
|
const locals = new Set<string>();
|
|
57
58
|
let unresolved = false;
|
|
58
59
|
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.join(", ");
|
|
60
|
+
const candidates: string[] = [];
|
|
61
|
+
for (const entry of srcset.split(SRCSET_SEPARATOR)) {
|
|
62
|
+
const parts = entry.trim().split(/\s+/);
|
|
63
|
+
const [url = ""] = parts;
|
|
64
|
+
const local = rewriteUrl(url);
|
|
65
|
+
if (local) {
|
|
66
|
+
count += 1;
|
|
67
|
+
parts[0] = local;
|
|
68
|
+
locals.add(local);
|
|
69
|
+
} else {
|
|
70
|
+
unresolved = true;
|
|
71
|
+
}
|
|
72
|
+
candidates.push(parts.join(" "));
|
|
73
|
+
}
|
|
74
|
+
const rewritten = candidates.join(", ");
|
|
75
75
|
|
|
76
76
|
/* Every candidate now points at the ONE file the family kept, so the attribute has become a
|
|
77
77
|
list of the same path repeated with different width descriptors — which lies to the browser
|
|
@@ -95,20 +95,20 @@ export function rewriteAssetUrls(
|
|
|
95
95
|
|
|
96
96
|
function rewriteStyleObject(style: JxStyle): void {
|
|
97
97
|
for (const [key, value] of Object.entries(style)) {
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
} else if (
|
|
101
|
-
rewriteStyleObject(value
|
|
98
|
+
if (isString(value) && value.includes("url(")) {
|
|
99
|
+
style[key] = rewriteCssUrls(value);
|
|
100
|
+
} else if (isNestedStyle(value) && key.startsWith("@")) {
|
|
101
|
+
rewriteStyleObject(value);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
function walk(node: JxElement): void {
|
|
107
|
-
const attrs = node.attributes
|
|
107
|
+
const attrs = node.attributes;
|
|
108
108
|
|
|
109
109
|
if (attrs) {
|
|
110
110
|
// Src attribute (img, video, audio, source, etc.)
|
|
111
|
-
if (
|
|
111
|
+
if (isString(attrs.src)) {
|
|
112
112
|
const rewritten = rewriteUrl(attrs.src);
|
|
113
113
|
if (rewritten) {
|
|
114
114
|
attrs.src = rewritten;
|
|
@@ -117,8 +117,8 @@ export function rewriteAssetUrls(
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// Href on non-anchor elements (could be link icon, etc.)
|
|
120
|
-
if (
|
|
121
|
-
const tag = (
|
|
120
|
+
if (isString(attrs.href)) {
|
|
121
|
+
const tag = (isString(node.tagName) ? node.tagName : "").toLowerCase();
|
|
122
122
|
if (tag !== "a") {
|
|
123
123
|
const rewritten = rewriteUrl(attrs.href);
|
|
124
124
|
if (rewritten) {
|
|
@@ -129,7 +129,7 @@ export function rewriteAssetUrls(
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// Poster attribute (video)
|
|
132
|
-
if (
|
|
132
|
+
if (isString(attrs.poster)) {
|
|
133
133
|
const rewritten = rewriteUrl(attrs.poster);
|
|
134
134
|
if (rewritten) {
|
|
135
135
|
attrs.poster = rewritten;
|
|
@@ -138,7 +138,7 @@ export function rewriteAssetUrls(
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
// Srcset attribute
|
|
141
|
-
if (
|
|
141
|
+
if (isString(attrs.srcset)) {
|
|
142
142
|
const result = rewriteSrcset(attrs.srcset);
|
|
143
143
|
if (result.collapsedTo === null) {
|
|
144
144
|
attrs.srcset = result.srcset;
|
|
@@ -161,7 +161,7 @@ export function rewriteAssetUrls(
|
|
|
161
161
|
// Recurse
|
|
162
162
|
if (Array.isArray(node.children)) {
|
|
163
163
|
for (const child of node.children) {
|
|
164
|
-
if (
|
|
164
|
+
if (!isString(child)) {
|
|
165
165
|
walk(child);
|
|
166
166
|
}
|
|
167
167
|
}
|
package/src/breakpoint-plan.ts
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
* @docs studio/design/breakpoints
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { IMPORT_MAX_BREAKPOINTS } from "@jxsuite/protocol/types";
|
|
24
|
+
|
|
23
25
|
/** One breakpoint: what to call it, what to emit, and the viewport width to sample it at. */
|
|
24
26
|
export interface Breakpoint {
|
|
25
27
|
/** Jx breakpoint name, e.g. `--768`. */
|
|
@@ -60,8 +62,11 @@ export const DEFAULT_BREAKPOINT_POLICY: BreakpointPolicy = {
|
|
|
60
62
|
rounding: "nearest",
|
|
61
63
|
};
|
|
62
64
|
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
+
/**
|
|
66
|
+
* The most breakpoints a project may be asked to keep. Past this, "limit" is not limiting. The
|
|
67
|
+
* value is the wire contract's, so a client that offers a count offers the same ceiling.
|
|
68
|
+
*/
|
|
69
|
+
export const MAX_BREAKPOINTS = IMPORT_MAX_BREAKPOINTS;
|
|
65
70
|
|
|
66
71
|
/** The viewport widths an override may name. Outside this a "width" is a typo, not a breakpoint. */
|
|
67
72
|
export const MIN_BREAKPOINT_WIDTH = 120;
|
package/src/capture.ts
CHANGED
|
@@ -20,20 +20,24 @@
|
|
|
20
20
|
export type PageArgs<Params extends unknown[]> = { [K in keyof Params]: Params[K] };
|
|
21
21
|
|
|
22
22
|
export interface ImportPage {
|
|
23
|
+
/**
|
|
24
|
+
* Resolves once navigation settles. Puppeteer resolves with the main response (or `null`), a
|
|
25
|
+
* stand-in may resolve with nothing; the import never reads the value either way.
|
|
26
|
+
*/
|
|
23
27
|
goto: (
|
|
24
28
|
url: string,
|
|
25
29
|
options?: {
|
|
26
30
|
waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
|
|
27
31
|
timeout?: number;
|
|
28
32
|
},
|
|
29
|
-
) => Promise<
|
|
30
|
-
setViewport: (viewport: { width: number; height: number }) => Promise<
|
|
33
|
+
) => Promise<object | null> | Promise<void>;
|
|
34
|
+
setViewport: (viewport: { width: number; height: number }) => Promise<void>;
|
|
31
35
|
evaluate: <Params extends unknown[], R>(
|
|
32
36
|
fn: (...params: PageArgs<Params>) => R,
|
|
33
37
|
...args: Params
|
|
34
38
|
) => Promise<Awaited<R>>;
|
|
35
39
|
screenshot: (options?: { fullPage?: boolean; type?: "png" }) => Promise<Uint8Array>;
|
|
36
|
-
close: () => Promise<
|
|
40
|
+
close: () => Promise<void>;
|
|
37
41
|
}
|
|
38
42
|
|
|
39
43
|
export interface ImportBrowser {
|
|
@@ -126,9 +130,11 @@ export async function capturePage(
|
|
|
126
130
|
|
|
127
131
|
const links: string[] = [];
|
|
128
132
|
const { origin } = location;
|
|
129
|
-
|
|
133
|
+
/* The selector names HTML anchors, whose `href` is the resolved URL string. An SVG `<a href>`
|
|
134
|
+
also matches and is read the same way; that has always been the case here. */
|
|
135
|
+
for (const a of document.querySelectorAll<HTMLAnchorElement>("a[href]")) {
|
|
130
136
|
try {
|
|
131
|
-
const { href } = new URL(
|
|
137
|
+
const { href } = new URL(a.href, location.href);
|
|
132
138
|
if (href.startsWith(origin) && !href.includes("#")) {
|
|
133
139
|
links.push(href);
|
|
134
140
|
}
|
package/src/cli.ts
CHANGED
|
@@ -179,11 +179,12 @@ if (maxDepth > 0) {
|
|
|
179
179
|
|
|
180
180
|
// The interactive flow runs inside an async function rather than via top-level await so a test can
|
|
181
181
|
// Dynamic-import this entry and await the same sequence (see create/index.ts for the rationale).
|
|
182
|
-
|
|
182
|
+
// The URL is passed in because a hoisted declaration cannot see the module-level check above.
|
|
183
|
+
async function main(siteUrl: string) {
|
|
183
184
|
try {
|
|
184
185
|
const result = await importSite(
|
|
185
186
|
{
|
|
186
|
-
url:
|
|
187
|
+
url: siteUrl,
|
|
187
188
|
outDir,
|
|
188
189
|
breakpoints,
|
|
189
190
|
maxDepth,
|
|
@@ -254,4 +255,4 @@ async function main() {
|
|
|
254
255
|
}
|
|
255
256
|
|
|
256
257
|
// oxlint-disable-next-line unicorn/prefer-top-level-await
|
|
257
|
-
export const ready = main();
|
|
258
|
+
export const ready = main(url);
|