@jxsuite/import 0.39.7 → 0.40.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/src/crawl.ts CHANGED
@@ -5,6 +5,12 @@ import type { ToJxResult } from "./to-jx.ts";
5
5
  import { captureStyles } from "./style-capture.ts";
6
6
  import { diffAllStyles, kebabToCamel } from "./style-diff.ts";
7
7
  import { extractMedia } from "./media-extract.ts";
8
+ import {
9
+ DEFAULT_BREAKPOINT_POLICY,
10
+ analyzeMediaQueries,
11
+ planBreakpoints,
12
+ } from "./breakpoint-plan.ts";
13
+ import type { Breakpoint, BreakpointPolicy } from "./breakpoint-plan.ts";
8
14
  import { applyStylesToTree } from "./apply-styles.ts";
9
15
  import { collectAssets } from "./asset-collect.ts";
10
16
  import { downloadAssets } from "./asset-download.ts";
@@ -24,6 +30,16 @@ export interface CrawlOptions {
24
30
  noScroll?: boolean;
25
31
  /** Capture a reference screenshot of each page before closing (for --verify). */
26
32
  captureScreenshots?: boolean;
33
+ /**
34
+ * Capture the whole scrollable page rather than the viewport (default true).
35
+ *
36
+ * It has to agree with what `verifyProject` screenshots on the other side. A viewport reference
37
+ * diffed against a full-page render is compared by padding the shorter image, so the two would
38
+ * disagree over everything below the fold before a single style was compared.
39
+ */
40
+ fullPageScreenshots?: boolean;
41
+ /** Which of the site's declared breakpoints the project keeps (see `breakpoint-plan.ts`). */
42
+ breakpoints?: BreakpointPolicy | undefined;
27
43
  onProgress?: (msg: string) => void;
28
44
  /** Abort the crawl between pages; the loop throws before capturing the next page. */
29
45
  signal?: AbortSignal;
@@ -178,6 +194,7 @@ export async function crawlSite(options: CrawlOptions): Promise<CrawlResult> {
178
194
  skipStyles,
179
195
  skipAssets,
180
196
  respectRobots,
197
+ fullPageScreenshots = true,
181
198
  onProgress = console.log,
182
199
  } = options;
183
200
 
@@ -202,6 +219,9 @@ export async function crawlSite(options: CrawlOptions): Promise<CrawlResult> {
202
219
  const skippedByRobots: string[] = [];
203
220
  const skippedByNodeCap: string[] = [];
204
221
  let mergedBreakpoints: Record<string, string> | undefined;
222
+ /* Decided on the first page that declares a width query, then reused. See ExtractMediaOptions.plan
223
+ — a per-page plan would union back up to the count the policy just refused. */
224
+ let breakpointPlan: Breakpoint[] | undefined;
205
225
  let mergedTokens: Record<string, string> | undefined;
206
226
  const allFontFaceRules: string[] = [];
207
227
  const fontRewriteMap = new Map<string, string>();
@@ -243,7 +263,9 @@ export async function crawlSite(options: CrawlOptions): Promise<CrawlResult> {
243
263
  skippedByNodeCap.push(entry.url);
244
264
  let screenshot: Buffer | undefined;
245
265
  if (options.captureScreenshots) {
246
- screenshot = Buffer.from(await capture.page.screenshot({ type: "png" }));
266
+ screenshot = Buffer.from(
267
+ await capture.page.screenshot({ fullPage: fullPageScreenshots, type: "png" }),
268
+ );
247
269
  }
248
270
  await capture.page.close();
249
271
 
@@ -277,11 +299,25 @@ export async function crawlSite(options: CrawlOptions): Promise<CrawlResult> {
277
299
  }
278
300
 
279
301
  if (styleResult.mediaQueries.length > 0) {
302
+ if (!breakpointPlan) {
303
+ const declared = analyzeMediaQueries(styleResult.mediaQueries);
304
+ breakpointPlan = planBreakpoints(
305
+ declared,
306
+ options.breakpoints ?? DEFAULT_BREAKPOINT_POLICY,
307
+ ).keep;
308
+ if (breakpointPlan.length < declared.length) {
309
+ onProgress?.(
310
+ ` ${declared.length} breakpoints declared, keeping ` +
311
+ `${breakpointPlan.length}: ${breakpointPlan.map((b) => b.name).join(", ")}\n`,
312
+ );
313
+ }
314
+ }
280
315
  const media = await extractMedia(
281
316
  capture.page,
282
317
  styleResult.elements,
283
318
  styleResult.uaDefaults,
284
319
  styleResult.mediaQueries,
320
+ { plan: breakpointPlan },
285
321
  );
286
322
  const bpCount = Object.keys(media.breakpoints).length;
287
323
  if (bpCount > 0) {
@@ -347,7 +383,9 @@ export async function crawlSite(options: CrawlOptions): Promise<CrawlResult> {
347
383
 
348
384
  let screenshot: Buffer | undefined;
349
385
  if (options.captureScreenshots) {
350
- screenshot = Buffer.from(await capture.page.screenshot({ type: "png" }));
386
+ screenshot = Buffer.from(
387
+ await capture.page.screenshot({ fullPage: fullPageScreenshots, type: "png" }),
388
+ );
351
389
  }
352
390
  await capture.page.close();
353
391
 
package/src/emit.ts CHANGED
@@ -2,8 +2,31 @@ import { join, dirname } from "node:path";
2
2
  import { mkdir } from "node:fs/promises";
3
3
  import type { JxElement } from "@jxsuite/schema/types";
4
4
  import { componentize } from "./componentize.ts";
5
+ import { stripClasses } from "./strip-classes.ts";
5
6
  import type { ComponentizeOptions, ComponentizeResult } from "./componentize.ts";
6
7
 
8
+ /**
9
+ * The `$media` key the base width lives under — a number of CSS pixels, not a query.
10
+ *
11
+ * Studio's canvas reads it to size the default artboard (`utils/canvas-media.ts`), and an import
12
+ * never wrote one, so every imported project's base canvas fell back to 320px while its narrowest
13
+ * real breakpoint was three times that. Mirrors `settings/contexts-section.ts`'s `BASE_KEY`.
14
+ */
15
+ const BASE_MEDIA_KEY = "--";
16
+
17
+ /** The width a `--<width>` breakpoint key names, for sorting. Non-numeric keys sort first. */
18
+ function mediaKeyWidth(key: string): number {
19
+ const width = Number(key.replace(/^--/, ""));
20
+ return Number.isFinite(width) ? width : -1;
21
+ }
22
+
23
+ export interface EmitResult {
24
+ /** Absolute paths of every file written. */
25
+ files: string[];
26
+ /** How many source-site class names were removed on the way out. */
27
+ classesStripped: number;
28
+ }
29
+
7
30
  export interface EmitOptions {
8
31
  outDir: string;
9
32
  title: string;
@@ -11,6 +34,8 @@ export interface EmitOptions {
11
34
  sourceUrl: string;
12
35
  /** $media breakpoints to seed into project.json (Phase 1). */
13
36
  breakpoints?: Record<string, string>;
37
+ /** The capture viewport width, written as the `$media` base entry. */
38
+ baseWidth?: number;
14
39
  /** Phase 4: componentization options. Pass false to skip. */
15
40
  componentizeOptions?: ComponentizeOptions | false;
16
41
  }
@@ -25,6 +50,8 @@ export interface MultiEmitOptions {
25
50
  layout?: JxElement | undefined;
26
51
  /** $media breakpoints to seed into project.json. */
27
52
  breakpoints?: Record<string, string> | undefined;
53
+ /** The capture viewport width, written as the `$media` base entry. */
54
+ baseWidth?: number | undefined;
28
55
  /** Phase 4: componentization options. Pass false to skip. */
29
56
  componentizeOptions?: ComponentizeOptions | false | undefined;
30
57
  /** Pre-computed componentization result (from AI pass). Overrides componentizeOptions. */
@@ -43,14 +70,16 @@ export async function emitProject({
43
70
  document,
44
71
  sourceUrl,
45
72
  breakpoints,
73
+ baseWidth,
46
74
  componentizeOptions,
47
- }: EmitOptions): Promise<{ files: string[] }> {
75
+ }: EmitOptions): Promise<EmitResult> {
48
76
  return emitMultiPageProject({
49
77
  outDir,
50
78
  title,
51
79
  sourceUrl,
52
80
  pages: new Map([["pages/index.json", document]]),
53
81
  breakpoints,
82
+ baseWidth,
54
83
  componentizeOptions,
55
84
  });
56
85
  }
@@ -58,35 +87,53 @@ export async function emitProject({
58
87
  export async function emitMultiPageProject({
59
88
  outDir,
60
89
  title,
61
- sourceUrl,
62
90
  pages,
63
91
  layout,
64
92
  breakpoints,
93
+ baseWidth,
65
94
  componentizeOptions,
66
95
  precomputedComponents,
67
96
  fontFaceRules,
68
97
  fontRewriteMap,
69
98
  styleTokens,
70
- }: MultiEmitOptions): Promise<{ files: string[] }> {
99
+ }: MultiEmitOptions): Promise<EmitResult> {
71
100
  await mkdir(join(outDir, "pages"), { recursive: true });
72
101
  await mkdir(join(outDir, "layouts"), { recursive: true });
73
102
  await mkdir(join(outDir, "components"), { recursive: true });
74
103
  await mkdir(join(outDir, "public"), { recursive: true });
75
104
 
105
+ /*
106
+ * Only keys the project schema declares (#228). `title` and `description` are page-level keys and
107
+ * `$style` is not a key at all, and all three made every imported project fail `jx validate` —
108
+ * and, because Studio's Contexts editor validates the WHOLE configuration before saving, made an
109
+ * imported project's base width uneditable, reporting `(root): must NOT have unevaluated
110
+ * properties` three times while naming nothing.
111
+ *
112
+ * The source URL has no schema-legal home and does not get one. `$head`'s description meta is the
113
+ * nearest candidate and the wrong one: it is the site's own user-facing description, and
114
+ * "Imported from …" is provenance, not a description of the site. The importer reports it instead.
115
+ */
76
116
  const projectJson: Record<string, unknown> = {
77
117
  name: title || "Imported Site",
78
- title: title || "Imported Site",
79
- description: `Imported from ${sourceUrl}`,
80
118
  imports: {},
81
119
  images: { optimize: false },
82
120
  };
83
121
 
122
+ if (baseWidth !== undefined) {
123
+ projectJson.$media = { [BASE_MEDIA_KEY]: `${Math.round(baseWidth)}px` };
124
+ }
84
125
  if (breakpoints && Object.keys(breakpoints).length > 0) {
85
- projectJson.$media = breakpoints;
126
+ /* Ascending, and the base first. `$media` is a map, so its order IS the order Studio's Contexts
127
+ list and the pane's size switcher offer these in; a crawl merged them in page-visit order,
128
+ which is not an order at all. */
129
+ const sorted = Object.entries(breakpoints).toSorted(
130
+ ([a], [b]) => mediaKeyWidth(a) - mediaKeyWidth(b),
131
+ );
132
+ projectJson.$media = { ...(projectJson.$media as object), ...Object.fromEntries(sorted) };
86
133
  }
87
134
 
88
135
  if (styleTokens && Object.keys(styleTokens).length > 0) {
89
- projectJson.$style = styleTokens;
136
+ projectJson.style = styleTokens;
90
137
  }
91
138
 
92
139
  const files: string[] = [];
@@ -123,12 +170,41 @@ export async function emitMultiPageProject({
123
170
  if (fontFaceRules && fontFaceRules.length > 0) {
124
171
  let fontCss = fontFaceRules.join("\n\n");
125
172
  if (fontRewriteMap) {
173
+ // A @font-face rule carries the url() form its AUTHOR wrote — usually root-relative
174
+ // ("/wp-content/.../lato.woff2"), sometimes protocol-relative. The rewrite map is keyed by the
175
+ // Absolute URL the downloader resolved, so matching on that alone rewrote nothing: every
176
+ // Downloaded font stayed unreferenced and the page fell back to system fonts.
177
+ //
178
+ // Replaced in ONE pass, longest form first. Replacing form by form re-scans text this loop
179
+ // Already rewrote, and a bare pathname ("/a.woff2") then matches inside the local path it
180
+ // Just produced ("/assets/fonts/a.woff2" to "/assets/fonts/assets/fonts/a.woff2").
181
+ const byForm = new Map<string, string>();
126
182
  for (const [originalUrl, localPath] of fontRewriteMap) {
127
- // Rewrite absolute URLs in font-face rules to relative local paths
128
- const relativePath = localPath.startsWith("public/")
183
+ const local = localPath.startsWith("public/")
129
184
  ? localPath.slice("public/".length)
130
185
  : localPath;
131
- fontCss = fontCss.replaceAll(originalUrl, `/${relativePath}`);
186
+ const href = local.startsWith("/") ? local : `/${local}`;
187
+ const forms = new Set([originalUrl]);
188
+ try {
189
+ const u = new URL(originalUrl);
190
+ forms.add(`//${u.host}${u.pathname}${u.search}`);
191
+ forms.add(`${u.pathname}${u.search}`);
192
+ } catch {
193
+ // Already a relative reference — the literal form is all there is.
194
+ }
195
+ for (const form of forms) {
196
+ if (form) {
197
+ byForm.set(form, href);
198
+ }
199
+ }
200
+ }
201
+ const forms = [...byForm.keys()].toSorted((a, b) => b.length - a.length);
202
+ if (forms.length > 0) {
203
+ const pattern = new RegExp(
204
+ forms.map((f) => f.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)).join("|"),
205
+ "g",
206
+ );
207
+ fontCss = fontCss.replaceAll(pattern, (m) => byForm.get(m) ?? m);
132
208
  }
133
209
  }
134
210
  const fontsDir = join(outDir, "public", "assets");
@@ -151,6 +227,22 @@ export async function emitMultiPageProject({
151
227
  await Bun.write(projectPath, `${JSON.stringify(projectJson, null, 2)}\n`);
152
228
  files.push(projectPath);
153
229
 
230
+ /*
231
+ * Classes go here, at the last possible moment, and that placement is the whole design.
232
+ *
233
+ * `componentize` groups by structure and `ai-componentize` reads the template to choose a name —
234
+ * `product-card` over `component-div-0` — and the class names are the strongest signal either has.
235
+ * Stripping earlier would cost that; stripping later is impossible. So both passes see the source
236
+ * site's classes, and nothing downstream of this line does.
237
+ */
238
+ let classesStripped = 0;
239
+ for (const compDoc of componentFiles.values()) {
240
+ classesStripped += stripClasses(compDoc);
241
+ }
242
+ for (const doc of finalPages.values()) {
243
+ classesStripped += stripClasses(doc);
244
+ }
245
+
154
246
  // Write components
155
247
  for (const [fileName, compDoc] of componentFiles) {
156
248
  const compPath = join(outDir, "components", fileName);
@@ -193,10 +285,11 @@ export async function emitMultiPageProject({
193
285
  ...(((layoutDoc as Record<string, unknown>).$elements as JxElement[]) ?? []),
194
286
  ];
195
287
  }
288
+ classesStripped += stripClasses(layoutDoc);
196
289
  await Bun.write(layoutPath, `${JSON.stringify(layoutDoc, null, 2)}\n`);
197
290
  files.push(layoutPath);
198
291
 
199
- return { files };
292
+ return { classesStripped, files };
200
293
  }
201
294
 
202
295
  function extractStateDefaults(node: JxElement | string, out: Record<string, string>) {
package/src/index.ts CHANGED
@@ -4,7 +4,19 @@ export { convertToJx } from "./to-jx.ts";
4
4
  export { emitProject, emitMultiPageProject } from "./emit.ts";
5
5
  export { captureStyles, captureStylesAtWidth, STYLE_ALLOWLIST } from "./style-capture.ts";
6
6
  export { diffStyles, diffAllStyles, computeMediaDelta, kebabToCamel } from "./style-diff.ts";
7
- export { analyzeMediaQueries, extractMedia } from "./media-extract.ts";
7
+ export { extractMedia } from "./media-extract.ts";
8
+ export {
9
+ DEFAULT_BREAKPOINT_POLICY,
10
+ analyzeMediaQueries,
11
+ skippedWidthQueries,
12
+ MAX_BREAKPOINTS,
13
+ MAX_BREAKPOINT_WIDTH,
14
+ MIN_BREAKPOINT_WIDTH,
15
+ breakpointName,
16
+ parseWidthQuery,
17
+ planBreakpoints,
18
+ } from "./breakpoint-plan.ts";
19
+ export { stripClasses } from "./strip-classes.ts";
8
20
  export { applyStylesToTree } from "./apply-styles.ts";
9
21
  export { collectAssets } from "./asset-collect.ts";
10
22
  export { downloadAssets } from "./asset-download.ts";
@@ -29,10 +41,16 @@ export type {
29
41
  ImportSiteResult,
30
42
  } from "./run.ts";
31
43
  export type { ToJxResult } from "./to-jx.ts";
32
- export type { EmitOptions, MultiEmitOptions } from "./emit.ts";
44
+ export type { EmitOptions, EmitResult, MultiEmitOptions } from "./emit.ts";
33
45
  export type { CapturedStyle, StyleCaptureResult } from "./style-capture.ts";
34
46
  export type { DiffedStyle } from "./style-diff.ts";
35
- export type { Breakpoint, MediaExtractionResult } from "./media-extract.ts";
47
+ export type { ExtractMediaOptions, MediaExtractionResult } from "./media-extract.ts";
48
+ export type {
49
+ Breakpoint,
50
+ BreakpointPlanResult,
51
+ BreakpointPolicy,
52
+ BreakpointRounding,
53
+ } from "./breakpoint-plan.ts";
36
54
  export type {
37
55
  DiscoveredAsset,
38
56
  AssetCollectionResult,
@@ -1,110 +1,86 @@
1
1
  /**
2
- * Extract @media breakpoints from captured CSS and orchestrate style re-capture at each breakpoint
3
- * width to produce per-node $media style deltas.
2
+ * Extract `@media` breakpoints from captured CSS and orchestrate style re-capture at each kept
3
+ * breakpoint width to produce per-node `$media` style deltas.
4
+ *
5
+ * The set of breakpoints a project keeps is decided in `breakpoint-plan.ts`, BEFORE anything is
6
+ * re-captured — each kept breakpoint costs a viewport change and a full style walk, so a limited
7
+ * import does less work rather than merely emitting less.
8
+ *
9
+ * A folded-away width is not sampled at all, and does not need to be. The computed style at a kept
10
+ * width is already the site's own cascade resolved AT that width, folded rules included; merging a
11
+ * separately-sampled delta on top of it could only disagree with the browser. The fold map exists
12
+ * so the import can SAY where a width went, not so its styles can be recovered.
4
13
  */
5
14
 
6
15
  import type { Page } from "puppeteer-core";
7
16
  import type { CapturedStyle } from "./style-capture.ts";
8
17
  import { captureStylesAtWidth } from "./style-capture.ts";
9
18
  import { computeMediaDelta } from "./style-diff.ts";
19
+ import { analyzeMediaQueries, planBreakpoints } from "./breakpoint-plan.ts";
20
+ import type { Breakpoint, BreakpointPolicy } from "./breakpoint-plan.ts";
10
21
  import type { DiffedStyle } from "./style-diff.ts";
11
22
 
12
- export interface Breakpoint {
13
- /** Jx breakpoint name, e.g. "--md" */
14
- name: string;
15
- /** CSS media query string, e.g. "(max-width: 768px)" */
16
- query: string;
17
- /** The width in px to test this breakpoint at */
18
- testWidth: number;
19
- }
23
+ /* The query analysis is pure and lives with the planner; it is re-exported here because this is the
24
+ module a caller reaches for when it is thinking about media. */
25
+ export { analyzeMediaQueries, skippedWidthQueries } from "./breakpoint-plan.ts";
26
+ export type {
27
+ Breakpoint,
28
+ BreakpointPolicy,
29
+ BreakpointPlanResult,
30
+ BreakpointRounding,
31
+ } from "./breakpoint-plan.ts";
20
32
 
21
33
  export interface MediaExtractionResult {
22
- /** Breakpoints to add to project.json.$media */
34
+ /** Breakpoints to add to `project.json`'s `$media`. */
23
35
  breakpoints: Record<string, string>;
24
- /** Per-breakpoint style deltas keyed by breakpoint name */
36
+ /** Per-breakpoint style deltas keyed by breakpoint name. */
25
37
  deltas: Record<string, DiffedStyle[]>;
26
38
  }
27
39
 
28
- /**
29
- * Parse a media query string and extract a pixel width if it's a simple min-width or max-width
30
- * query. Returns null for complex/non-size queries.
31
- */
32
- function parseWidthQuery(query: string): { type: "min" | "max"; width: number } | null {
33
- const minMatch = query.match(/^\s*\(\s*min-width\s*:\s*([\d.]+)px\s*\)\s*$/);
34
- if (minMatch) {
35
- return { type: "min", width: Number(minMatch[1]) };
36
- }
37
- const maxMatch = query.match(/^\s*\(\s*max-width\s*:\s*([\d.]+)px\s*\)\s*$/);
38
- if (maxMatch) {
39
- return { type: "max", width: Number(maxMatch[1]) };
40
- }
41
- return null;
42
- }
43
-
44
- /** Generate a Jx breakpoint name from a width, e.g. 768 → "--768" */
45
- function breakpointName(width: number): string {
46
- return `--${width}`;
47
- }
48
-
49
- /**
50
- * Analyze discovered @media queries and produce a set of testable breakpoints. Only picks up simple
51
- * min-width / max-width queries — complex queries (orientation, hover, prefers-*) are logged and
52
- * skipped.
53
- */
54
- export function analyzeMediaQueries(queries: string[]): Breakpoint[] {
55
- const seen = new Set<number>();
56
- const breakpoints: Breakpoint[] = [];
57
-
58
- for (const query of queries) {
59
- const parsed = parseWidthQuery(query);
60
- if (!parsed) {
61
- continue;
62
- }
63
- if (seen.has(parsed.width)) {
64
- continue;
65
- }
66
- seen.add(parsed.width);
67
-
68
- // For max-width: test AT that width (it's the upper bound)
69
- // For min-width: test AT that width (it's the lower bound — styles kick in here)
70
- breakpoints.push({
71
- name: breakpointName(parsed.width),
72
- query,
73
- testWidth: parsed.width,
74
- });
75
- }
76
-
77
- // Sort by width ascending
78
- breakpoints.sort((a, b) => a.testWidth - b.testWidth);
79
- return breakpoints;
40
+ export interface ExtractMediaOptions {
41
+ /** The base viewport width to restore after extraction. */
42
+ originalWidth?: number;
43
+ /** Which of the discovered breakpoints to keep (default: the three-breakpoint limit). */
44
+ policy?: BreakpointPolicy | undefined;
45
+ /**
46
+ * An already-decided plan, used verbatim instead of planning from this page's own queries.
47
+ *
48
+ * A crawl needs this. Planning per page would let page 2 keep a width page 1 folded away, so the
49
+ * project's `$media` would be the UNION of several plans — the very thing a limit is for — and
50
+ * the breakpoint names a node carries would depend on which page it came from. One plan, decided
51
+ * on the first page that declares any width, is what keeps the names meaning one thing.
52
+ */
53
+ plan?: readonly Breakpoint[] | undefined;
80
54
  }
81
55
 
82
56
  /**
83
- * Extract $media style deltas by re-capturing at each breakpoint width.
57
+ * Extract `$media` style deltas by re-capturing at each KEPT breakpoint width.
84
58
  *
85
- * @param page - Puppeteer page (must still have the target page loaded)
86
- * @param baseElements - Style capture from the base viewport (1440px)
87
- * @param uaDefaults - UA-default baselines per tagName
88
- * @param mediaQueries - @media queries discovered in the page's stylesheets
89
- * @param originalWidth - The base viewport width to restore after extraction
59
+ * @param {Page} page - Puppeteer page (must still have the target page loaded)
60
+ * @param {CapturedStyle[]} baseElements - Style capture from the base viewport
61
+ * @param {Record<string, Record<string, string>>} uaDefaults - UA-default baselines per tagName
62
+ * @param {readonly string[]} mediaQueries - `@media` queries discovered in the page's stylesheets
63
+ * @param {ExtractMediaOptions} [options]
64
+ * @returns {Promise<MediaExtractionResult>}
90
65
  */
91
66
  export async function extractMedia(
92
67
  page: Page,
93
68
  baseElements: CapturedStyle[],
94
69
  uaDefaults: Record<string, Record<string, string>>,
95
- mediaQueries: string[],
96
- originalWidth = 1440,
70
+ mediaQueries: readonly string[],
71
+ options: ExtractMediaOptions = {},
97
72
  ): Promise<MediaExtractionResult> {
98
- const breakpoints = analyzeMediaQueries(mediaQueries);
73
+ const { originalWidth = 1440, plan, policy } = options;
74
+ const keep = plan ?? planBreakpoints(analyzeMediaQueries(mediaQueries), policy).keep;
99
75
 
100
- if (breakpoints.length === 0) {
76
+ if (keep.length === 0) {
101
77
  return { breakpoints: {}, deltas: {} };
102
78
  }
103
79
 
104
80
  const projectBreakpoints: Record<string, string> = {};
105
81
  const allDeltas: Record<string, DiffedStyle[]> = {};
106
82
 
107
- for (const bp of breakpoints) {
83
+ for (const bp of keep) {
108
84
  const bpElements = await captureStylesAtWidth(page, bp.testWidth);
109
85
  const deltas = computeMediaDelta(baseElements, bpElements, uaDefaults);
110
86