@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/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 (typeof node === "string") {
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 as JxElement | string);
49
+ n += countNodes(c);
49
50
  }
50
51
  }
51
52
  return n;
52
53
  }
53
54
 
54
- function stripTags(nodes: (JxElement | string)[]): {
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 (typeof node === "string") {
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 (typeof node.textContent === "string") {
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 as (JxElement | string)[]);
85
- node.children = sub.cleaned as JxElement[];
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 as JxElement[],
105
+ children: cleaned,
102
106
  };
103
107
 
104
108
  return {
@@ -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 (typeof value !== "string" || value.length === 0) {
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, unknown> | undefined} attributes
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, unknown> | undefined,
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 (typeof value !== "string") {
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
- if (typeof (page as Partial<Page>).on !== "function") {
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(error instanceof Error ? error.message : String(error));
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: Record<string, string> = {
207
- ".avif": "image/avif",
208
- ".css": "text/css",
209
- ".gif": "image/gif",
210
- ".htm": "text/html; charset=utf-8",
211
- ".html": "text/html; charset=utf-8",
212
- ".ico": "image/x-icon",
213
- ".jpeg": "image/jpeg",
214
- ".jpg": "image/jpeg",
215
- ".js": "text/javascript; charset=utf-8",
216
- ".json": "application/json; charset=utf-8",
217
- ".map": "application/json; charset=utf-8",
218
- ".mjs": "text/javascript; charset=utf-8",
219
- ".otf": "font/otf",
220
- ".png": "image/png",
221
- ".svg": "image/svg+xml",
222
- ".ttf": "font/ttf",
223
- ".txt": "text/plain; charset=utf-8",
224
- ".webmanifest": "application/manifest+json",
225
- ".webp": "image/webp",
226
- ".woff": "font/woff",
227
- ".woff2": "font/woff2",
228
- ".xml": "application/xml; charset=utf-8",
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[extname(filePath).toLowerCase()] ?? "application/octet-stream";
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 err = error as Error;
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: ${err.message}`,
331
+ error: `Build failed: ${message}`,
319
332
  })),
320
333
  averageFidelity: 0,
321
334
  reportDir,
322
- buildErrors: [`Build failed: ${err.message}`],
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 err = error as Error;
396
- onProgress(` Error: ${err.message}`);
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: err.message,
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
- (buildErrors ?? []).length === 0 && errored.length === 0 && averageFidelity >= minFidelity;
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: 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: buildErrors ?? [],
467
+ buildErrors,
455
468
  passed,
456
469
  };
457
470
  } finally {