@jxsuite/import 0.40.9 → 0.40.12
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 +8 -5
- 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/to-jx.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { htmlToJx } from "@jxsuite/markup/html-to-jx";
|
|
2
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
2
3
|
import type { JxElement } from "@jxsuite/schema/types";
|
|
3
4
|
|
|
4
5
|
export interface ToJxResult {
|
|
@@ -39,27 +40,30 @@ export const STRIP_TAGS = new Set([
|
|
|
39
40
|
]);
|
|
40
41
|
|
|
41
42
|
function countNodes(node: JxElement | string): number {
|
|
42
|
-
if (
|
|
43
|
+
if (isString(node)) {
|
|
43
44
|
return 0;
|
|
44
45
|
}
|
|
45
46
|
let n = 1;
|
|
46
47
|
if (Array.isArray(node.children)) {
|
|
47
48
|
for (const c of node.children) {
|
|
48
|
-
n += countNodes(c
|
|
49
|
+
n += countNodes(c);
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
return n;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
|
|
55
|
+
/** A converted subtree with the stripped tags removed, and the inline CSS harvested from them. */
|
|
56
|
+
interface StrippedNodes {
|
|
55
57
|
cleaned: (JxElement | string)[];
|
|
56
58
|
styles: string[];
|
|
57
|
-
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stripTags(nodes: (JxElement | string)[]): StrippedNodes {
|
|
58
62
|
const cleaned: (JxElement | string)[] = [];
|
|
59
63
|
const styles: string[] = [];
|
|
60
64
|
|
|
61
65
|
for (const node of nodes) {
|
|
62
|
-
if (
|
|
66
|
+
if (isString(node)) {
|
|
63
67
|
cleaned.push(node);
|
|
64
68
|
continue;
|
|
65
69
|
}
|
|
@@ -69,7 +73,7 @@ function stripTags(nodes: (JxElement | string)[]): {
|
|
|
69
73
|
// Collect inline <style> content for Phase 1. Style elements hold raw text, which htmlToJx
|
|
70
74
|
// Always collapses into textContent (never string children).
|
|
71
75
|
if (tag === "style") {
|
|
72
|
-
if (
|
|
76
|
+
if (isString(node.textContent)) {
|
|
73
77
|
styles.push(node.textContent);
|
|
74
78
|
}
|
|
75
79
|
continue;
|
|
@@ -81,8 +85,8 @@ function stripTags(nodes: (JxElement | string)[]): {
|
|
|
81
85
|
|
|
82
86
|
// Recurse into children
|
|
83
87
|
if (Array.isArray(node.children)) {
|
|
84
|
-
const sub = stripTags(node.children
|
|
85
|
-
node.children = sub.cleaned
|
|
88
|
+
const sub = stripTags(node.children);
|
|
89
|
+
node.children = sub.cleaned;
|
|
86
90
|
styles.push(...sub.styles);
|
|
87
91
|
}
|
|
88
92
|
|
|
@@ -98,7 +102,7 @@ export function convertToJx(bodyHtml: string): ToJxResult {
|
|
|
98
102
|
|
|
99
103
|
const document: JxElement = {
|
|
100
104
|
tagName: "div",
|
|
101
|
-
children: cleaned
|
|
105
|
+
children: cleaned,
|
|
102
106
|
};
|
|
103
107
|
|
|
104
108
|
return {
|
package/src/toggle-algebra.ts
CHANGED
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
* rules, which they ship correctly. When the algebra does not close, nothing is rewritten.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import { isString } from "@jxsuite/schema/guards";
|
|
32
|
+
import type { JxAttributeValue } from "@jxsuite/schema/types";
|
|
33
|
+
|
|
31
34
|
/** A JavaScript identifier, which is what every one of these shapes is built around. */
|
|
32
35
|
const IDENT = String.raw`[A-Za-z_$][A-Za-z0-9_$]*`;
|
|
33
36
|
|
|
@@ -93,7 +96,7 @@ export function classifyToggleValue(
|
|
|
93
96
|
value: string,
|
|
94
97
|
knownIdents?: ReadonlySet<string>,
|
|
95
98
|
): ToggleRole | null {
|
|
96
|
-
if (
|
|
99
|
+
if (value.length === 0) {
|
|
97
100
|
return null;
|
|
98
101
|
}
|
|
99
102
|
|
|
@@ -141,12 +144,12 @@ export function classifyToggleValue(
|
|
|
141
144
|
* declaration outranks a toggle, which outranks a predicate, because that is the order in which
|
|
142
145
|
* they identify the node: a root, then a control, then a panel.
|
|
143
146
|
*
|
|
144
|
-
* @param {Record<string,
|
|
147
|
+
* @param {Record<string, JxAttributeValue> | undefined} attributes
|
|
145
148
|
* @param {ReadonlySet<string>} [knownIdents] - Idents a DECLARE in scope has introduced
|
|
146
149
|
* @returns {ToggleRole | null}
|
|
147
150
|
*/
|
|
148
151
|
export function roleOfElement(
|
|
149
|
-
attributes: Record<string,
|
|
152
|
+
attributes: Record<string, JxAttributeValue> | undefined,
|
|
150
153
|
knownIdents?: ReadonlySet<string>,
|
|
151
154
|
): ToggleRole | null {
|
|
152
155
|
if (!attributes) {
|
|
@@ -155,7 +158,7 @@ export function roleOfElement(
|
|
|
155
158
|
const rank = { assign: 1, compare: 2, declare: 0 };
|
|
156
159
|
let best: ToggleRole | null = null;
|
|
157
160
|
for (const value of Object.values(attributes)) {
|
|
158
|
-
if (
|
|
161
|
+
if (!isString(value)) {
|
|
159
162
|
continue;
|
|
160
163
|
}
|
|
161
164
|
const role = classifyToggleValue(value, knownIdents);
|
package/src/verify.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { extname, join } from "node:path";
|
|
11
11
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { isFunction } from "@jxsuite/schema/guards";
|
|
12
13
|
import type { Browser, Page } from "puppeteer-core";
|
|
13
14
|
import { launchBrowser, closeBrowser } from "./browser-local.ts";
|
|
14
15
|
import { diffScreenshots } from "./screenshot-diff.ts";
|
|
@@ -122,6 +123,11 @@ interface PageDiagnostics {
|
|
|
122
123
|
failedRequests: string[];
|
|
123
124
|
}
|
|
124
125
|
|
|
126
|
+
/** The message of a thrown value, which may be any value at all rather than an Error. */
|
|
127
|
+
function describeError(cause: unknown): string {
|
|
128
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
129
|
+
}
|
|
130
|
+
|
|
125
131
|
/**
|
|
126
132
|
* Listen for the two things a fidelity percentage cannot tell you: what the page logged, and what
|
|
127
133
|
* it asked for and did not get.
|
|
@@ -132,7 +138,9 @@ interface PageDiagnostics {
|
|
|
132
138
|
*/
|
|
133
139
|
function watchPage(page: Page): PageDiagnostics {
|
|
134
140
|
const diagnostics: PageDiagnostics = { consoleErrors: [], failedRequests: [] };
|
|
135
|
-
|
|
141
|
+
/* SAFETY: Widening to Partial<Page> only admits what the doc comment above describes: a stand-in
|
|
142
|
+
page that screenshots but has no event emitter is passed where a Page is expected. */
|
|
143
|
+
if (!isFunction((page as Partial<Page>).on)) {
|
|
136
144
|
return diagnostics;
|
|
137
145
|
}
|
|
138
146
|
page.on("console", (msg) => {
|
|
@@ -143,7 +151,7 @@ function watchPage(page: Page): PageDiagnostics {
|
|
|
143
151
|
// `pageerror` carries `Error | unknown`: a page can reject with anything at all, and a thrown
|
|
144
152
|
// String is still a page that broke.
|
|
145
153
|
page.on("pageerror", (error) => {
|
|
146
|
-
diagnostics.consoleErrors.push(
|
|
154
|
+
diagnostics.consoleErrors.push(describeError(error));
|
|
147
155
|
});
|
|
148
156
|
page.on("requestfailed", (request) => {
|
|
149
157
|
diagnostics.failedRequests.push(`failed ${request.url()}`);
|
|
@@ -203,39 +211,44 @@ async function buildProject(projectDir: string): Promise<{ distDir: string; erro
|
|
|
203
211
|
* will not use a font it was handed as one, which cost the clone fidelity points that were not the
|
|
204
212
|
* importer's fault at all (issue #232).
|
|
205
213
|
*/
|
|
206
|
-
const CONTENT_TYPES
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
214
|
+
const CONTENT_TYPES = new Map<string, string>(
|
|
215
|
+
Object.entries({
|
|
216
|
+
".avif": "image/avif",
|
|
217
|
+
".css": "text/css",
|
|
218
|
+
".gif": "image/gif",
|
|
219
|
+
".htm": "text/html; charset=utf-8",
|
|
220
|
+
".html": "text/html; charset=utf-8",
|
|
221
|
+
".ico": "image/x-icon",
|
|
222
|
+
".jpeg": "image/jpeg",
|
|
223
|
+
".jpg": "image/jpeg",
|
|
224
|
+
".js": "text/javascript; charset=utf-8",
|
|
225
|
+
".json": "application/json; charset=utf-8",
|
|
226
|
+
".map": "application/json; charset=utf-8",
|
|
227
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
228
|
+
".otf": "font/otf",
|
|
229
|
+
".png": "image/png",
|
|
230
|
+
".svg": "image/svg+xml",
|
|
231
|
+
".ttf": "font/ttf",
|
|
232
|
+
".txt": "text/plain; charset=utf-8",
|
|
233
|
+
".webmanifest": "application/manifest+json",
|
|
234
|
+
".webp": "image/webp",
|
|
235
|
+
".woff": "font/woff",
|
|
236
|
+
".woff2": "font/woff2",
|
|
237
|
+
".xml": "application/xml; charset=utf-8",
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
/** A running static server and the origin it answers on. */
|
|
242
|
+
export interface ServedDirectory {
|
|
243
|
+
server: ReturnType<typeof Bun.serve>;
|
|
244
|
+
baseUrl: string;
|
|
245
|
+
}
|
|
230
246
|
|
|
231
247
|
/**
|
|
232
248
|
* Serve a directory over HTTP using Bun's built-in server. Returns the server instance and the base
|
|
233
249
|
* URL.
|
|
234
250
|
*/
|
|
235
|
-
export function serveDirectory(dir: string): {
|
|
236
|
-
server: ReturnType<typeof Bun.serve>;
|
|
237
|
-
baseUrl: string;
|
|
238
|
-
} {
|
|
251
|
+
export function serveDirectory(dir: string): ServedDirectory {
|
|
239
252
|
const server = Bun.serve({
|
|
240
253
|
port: 0,
|
|
241
254
|
async fetch(req) {
|
|
@@ -250,7 +263,7 @@ export function serveDirectory(dir: string): {
|
|
|
250
263
|
const file = Bun.file(filePath);
|
|
251
264
|
if (await file.exists()) {
|
|
252
265
|
const contentType =
|
|
253
|
-
CONTENT_TYPES
|
|
266
|
+
CONTENT_TYPES.get(extname(filePath).toLowerCase()) ?? "application/octet-stream";
|
|
254
267
|
return new Response(file, { headers: { "Content-Type": contentType } });
|
|
255
268
|
}
|
|
256
269
|
return new Response("Not Found", { status: 404 });
|
|
@@ -302,7 +315,7 @@ export async function verifyProject(opts: VerifyOptions): Promise<VerifyResult>
|
|
|
302
315
|
onProgress(` Build completed with ${buildErrors.length} error(s)`);
|
|
303
316
|
}
|
|
304
317
|
} catch (error) {
|
|
305
|
-
const
|
|
318
|
+
const message = describeError(error);
|
|
306
319
|
return {
|
|
307
320
|
pages: [...pages.entries()].map(([route, ref]) => ({
|
|
308
321
|
route,
|
|
@@ -315,11 +328,11 @@ export async function verifyProject(opts: VerifyOptions): Promise<VerifyResult>
|
|
|
315
328
|
renderedScreenshotPath: "",
|
|
316
329
|
consoleErrors: [],
|
|
317
330
|
failedRequests: [],
|
|
318
|
-
error: `Build failed: ${
|
|
331
|
+
error: `Build failed: ${message}`,
|
|
319
332
|
})),
|
|
320
333
|
averageFidelity: 0,
|
|
321
334
|
reportDir,
|
|
322
|
-
buildErrors: [`Build failed: ${
|
|
335
|
+
buildErrors: [`Build failed: ${message}`],
|
|
323
336
|
passed: false,
|
|
324
337
|
};
|
|
325
338
|
}
|
|
@@ -392,8 +405,8 @@ export async function verifyProject(opts: VerifyOptions): Promise<VerifyResult>
|
|
|
392
405
|
failedRequests: diagnostics.failedRequests,
|
|
393
406
|
});
|
|
394
407
|
} catch (error) {
|
|
395
|
-
const
|
|
396
|
-
onProgress(` Error: ${
|
|
408
|
+
const message = describeError(error);
|
|
409
|
+
onProgress(` Error: ${message}`);
|
|
397
410
|
results.push({
|
|
398
411
|
route,
|
|
399
412
|
sourceUrl: ref.sourceUrl,
|
|
@@ -405,7 +418,7 @@ export async function verifyProject(opts: VerifyOptions): Promise<VerifyResult>
|
|
|
405
418
|
renderedScreenshotPath: "",
|
|
406
419
|
consoleErrors: [],
|
|
407
420
|
failedRequests: [],
|
|
408
|
-
error:
|
|
421
|
+
error: message,
|
|
409
422
|
});
|
|
410
423
|
}
|
|
411
424
|
}
|
|
@@ -430,7 +443,7 @@ export async function verifyProject(opts: VerifyOptions): Promise<VerifyResult>
|
|
|
430
443
|
*/
|
|
431
444
|
const errored = results.filter((r) => r.error);
|
|
432
445
|
const passed =
|
|
433
|
-
|
|
446
|
+
buildErrors.length === 0 && errored.length === 0 && averageFidelity >= minFidelity;
|
|
434
447
|
|
|
435
448
|
const report = {
|
|
436
449
|
timestamp: new Date().toISOString(),
|
|
@@ -443,7 +456,7 @@ export async function verifyProject(opts: VerifyOptions): Promise<VerifyResult>
|
|
|
443
456
|
averageFidelity,
|
|
444
457
|
passed,
|
|
445
458
|
pages: results,
|
|
446
|
-
buildErrors
|
|
459
|
+
buildErrors,
|
|
447
460
|
};
|
|
448
461
|
writeFileSync(join(reportDir, "report.json"), JSON.stringify(report, null, 2));
|
|
449
462
|
|
|
@@ -451,7 +464,7 @@ export async function verifyProject(opts: VerifyOptions): Promise<VerifyResult>
|
|
|
451
464
|
pages: results,
|
|
452
465
|
averageFidelity,
|
|
453
466
|
reportDir,
|
|
454
|
-
buildErrors
|
|
467
|
+
buildErrors,
|
|
455
468
|
passed,
|
|
456
469
|
};
|
|
457
470
|
} finally {
|