@tokenoftrust/storefront-runner 2.0.49 → 2.0.51

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.0.49",
3
+ "version": "2.0.51",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -20,7 +20,7 @@
20
20
  "./integration/native-ga4-sandbox-asset": "./src/integration/native-ga4-sandbox-asset.mjs"
21
21
  },
22
22
  "dependencies": {
23
- "lightningcss": "^1.32.0",
23
+ "css-tree": "2.2.1",
24
24
  "parse5": "^7.2.1"
25
25
  },
26
26
  "scripts": {
@@ -31,6 +31,7 @@
31
31
  "generate:managed-analytics-schemas": "node --experimental-strip-types scripts/generate-managed-analytics-api-v1-schemas.mjs"
32
32
  },
33
33
  "devDependencies": {
34
+ "@types/css-tree": "^3.2.0",
34
35
  "ajv": "^8.20.0",
35
36
  "typescript": "^5.6.3",
36
37
  "vitest": "^2.1.8"
@@ -1,4 +1,4 @@
1
- import { transform, transformStyleAttribute, type CustomAtRules, type Declaration, type Image, type TokenOrValue, type Url, type Visitor } from "lightningcss";
1
+ import { ident, parse, string as cssString, tokenize, tokenNames, url as cssUrl } from "css-tree";
2
2
  import { parseFragment, type DefaultTreeAdapterMap } from "parse5";
3
3
 
4
4
  /**
@@ -394,64 +394,46 @@ function rememberCssLocator(refs: ExtractedReference[], locator: string): boolea
394
394
  return true;
395
395
  }
396
396
 
397
- function urlLocationKey(url: Url): string {
398
- return `${url.loc.line}:${url.loc.column}`;
399
- }
400
-
401
- function cssFunctionName(name: string): string {
402
- return name.toLowerCase();
403
- }
404
-
405
- function declarationPropertyName(declaration: Declaration): string {
406
- // Custom properties keep author case. Image properties match case-insensitively.
407
- const name = declaration.property === "unparsed"
408
- ? declaration.value.propertyId.property
409
- : declaration.property === "custom"
410
- ? String(declaration.value.name)
411
- : declaration.property;
412
- return name.toLowerCase();
413
- }
414
-
415
- /** Quoted URL() / uRl() inside an unparsed image() is a nested function. Its source is the first string. */
416
- function quotedUrlFunctionLocator(nodes: readonly TokenOrValue[]): string | null {
417
- for (const argument of nodes) {
418
- if (argument.type === "token" && argument.value.type === "string") return String(argument.value.value);
419
- }
420
- return null;
421
- }
422
-
423
- function suppressImageFunctionUrls(
424
- nodes: readonly TokenOrValue[],
425
- locations: Set<string>,
426
- walkVariableFallbacks = false,
427
- ): void {
428
- for (const argument of nodes) {
429
- if (argument.type === "url") {
430
- locations.add(urlLocationKey(argument.value));
431
- continue;
432
- }
433
- if (argument.type === "function") {
434
- suppressImageFunctionUrls(argument.value.arguments, locations, walkVariableFallbacks);
435
- continue;
397
+ type CssScanNode = {
398
+ type: string;
399
+ name?: string;
400
+ value?: string;
401
+ property?: string;
402
+ children?: CssScanNode[];
403
+ block?: CssScanNode | null;
404
+ };
405
+
406
+ function cssChildren(node: CssScanNode): CssScanNode[] {
407
+ return node.children ?? [];
408
+ }
409
+
410
+ function rawEscapedUrlReferences(raw: string, refs: ExtractedReference[]): void {
411
+ const tokens: Array<{ name: string; start: number; end: number }> = [];
412
+ tokenize(raw, (type, start, end) => tokens.push({ name: tokenNames[type] ?? "", start, end }));
413
+ for (let index = 0; index < tokens.length; index++) {
414
+ const token = tokens[index]!;
415
+ const rawName = raw.slice(token.start, token.end - 1);
416
+ if (token.name !== "function-token" || !rawName.includes("\\") || ident.decode(rawName).toLowerCase() !== "url") continue;
417
+ let depth = 1;
418
+ for (let next = index + 1; next < tokens.length; next++) {
419
+ const nextToken = tokens[next]!;
420
+ if (nextToken.name === "function-token" || nextToken.name === "(-token") depth++;
421
+ if (nextToken.name === ")-token") depth--;
422
+ if (depth !== 0) continue;
423
+ const argument = raw.slice(token.end, nextToken.start).trim();
424
+ const locator = argument.startsWith('"') || argument.startsWith("'")
425
+ ? cssString.decode(argument)
426
+ : cssUrl.decode(`url(${argument})`);
427
+ rememberCssLocator(refs, locator);
428
+ index = next;
429
+ break;
436
430
  }
437
- // image-set suppression stays on nested functions. image() also suppresses var()/env() fallback urls.
438
- if (!walkVariableFallbacks || (argument.type !== "var" && argument.type !== "env")) continue;
439
- const fallback = argument.value.fallback;
440
- if (fallback) suppressImageFunctionUrls(fallback, locations, true);
441
- }
442
- }
443
-
444
- /** url() nodes nested in an unparsed image-set option are not that option. Direct url() arguments stay visible. */
445
- function suppressNestedFunctionUrls(nodes: readonly TokenOrValue[], locations: Set<string>): void {
446
- for (const argument of nodes) {
447
- if (argument.type === "function") suppressImageFunctionUrls(argument.value.arguments, locations);
448
431
  }
449
432
  }
450
433
 
451
434
  function deferredFunctionName(name: string): "var" | "env" | "cross-fade" | null {
452
- const normalized = cssFunctionName(name);
435
+ const normalized = name.toLowerCase();
453
436
  if (normalized === "var" || normalized === "env") return normalized;
454
- // -webkit-cross-fade is cross-fade. Any vendor prefix stays unresolved.
455
437
  if (normalized === "cross-fade" || /^-[a-z]+-cross-fade$/.test(normalized)) return "cross-fade";
456
438
  return null;
457
439
  }
@@ -460,170 +442,150 @@ function rememberDeferred(refs: ExtractedReference[], name: "var" | "env" | "cro
460
442
  refs.push({ locator: `css-function:${name}`, surface: "css-url", deferredFunction: name });
461
443
  }
462
444
 
463
- /** One kept locator. type(), format(), and a discarded non-image do not consume it. var(), env(), and cross-fade() consume it without walking children. Nested image() keeps its own one source. */
464
- function recordImageFunctionSource(refs: ExtractedReference[], nodes: readonly TokenOrValue[]): boolean {
465
- for (const argument of nodes) {
466
- if (argument.type === "url") {
467
- if (rememberCssLocator(refs, argument.value.url)) return true;
468
- continue;
469
- }
470
- if (argument.type === "token" && argument.value.type === "string") {
471
- if (rememberCssLocator(refs, String(argument.value.value))) return true;
472
- continue;
473
- }
474
- // The visitor records the deferred dependency. Children are not a source.
475
- if (argument.type === "var" || argument.type === "env") return true;
476
- if (argument.type !== "function") continue;
477
- const name = cssFunctionName(argument.value.name);
478
- if (name === "url") {
479
- const locator = quotedUrlFunctionLocator(argument.value.arguments);
480
- if (locator != null && rememberCssLocator(refs, locator)) return true;
481
- continue;
482
- }
483
- // MIME tags, not image sources. Recursing would keep "image/png" and consume the one slot.
484
- if (name === "type" || name === "format") continue;
485
- if (deferredFunctionName(name)) return true;
486
- if (recordImageFunctionSource(refs, argument.value.arguments)) return true;
487
- }
488
- return false;
489
- }
490
-
491
445
  function adoptCssReferences(refs: ExtractedReference[], cssRefs: readonly ExtractedReference[], afterBase: boolean): void {
492
446
  for (const ref of cssRefs) refs.push(afterBase ? { ...ref, afterBase: true } : ref);
493
447
  }
494
448
 
495
- function rememberCssImage(refs: ExtractedReference[], image: Image): void {
496
- if (image.type === "url") {
497
- rememberCssLocator(refs, image.value.url);
498
- return;
499
- }
500
- if (image.type !== "image-set") return;
501
- for (const option of image.value.options) rememberCssImage(refs, option.image);
502
- }
503
-
449
+ /**
450
+ * CSS extraction uses a JavaScript parser because this module is imported by
451
+ * Worker routes. A native CSS binding cannot be evaluated by workerd, which
452
+ * would prevent every route sharing its bundle chunk from loading.
453
+ */
504
454
  function cssReferences(css: string, kind: "stylesheet" | "declaration"): ExtractedReference[] {
505
455
  if (!css) return [];
506
456
  const refs: ExtractedReference[] = [];
507
- const fontFaceUrls: string[] = [];
508
- // url() nodes anywhere inside image() are still walked by the Url visitor after Function.
509
- // The visitor rebuilds a fresh object per callback, so identity can't dedupe — key on source
510
- // position, which is stable for the same node. One image() keeps one source; the rest stay suppressed.
511
- const imageFunctionUrlLocations = new Set<string>();
512
- // Function enter runs before children. "image" means this image() recorded its one source;
513
- // a nested image() visit must not record again. "image-set" means a direct string token is an option.
514
- // "deferred" is var(), env(), or cross-fade(): their children are not resolved.
515
- const frames: Array<"image" | "image-set" | "deferred" | "skip"> = [];
516
- let valueDeferDepth = 0;
517
- const propertyStack: string[] = [];
518
- const deferredOpen = (): boolean => valueDeferDepth > 0 || frames.includes("deferred");
519
- const inImageValue = (): boolean => {
520
- if (frames.includes("image") || frames.includes("image-set")) return true;
521
- const property = propertyStack.at(-1);
522
- return property != null && IMAGE_VALUE_PROPERTIES.has(property);
457
+ const parseValue = (value: string): CssScanNode | null => {
458
+ try {
459
+ const parsed = parse(value, { context: "value", list: false }) as unknown as CssScanNode;
460
+ return parsed.type === "Value" ? parsed : null;
461
+ } catch {
462
+ return null;
463
+ }
523
464
  };
524
- const noteDeferred = (name: "var" | "env" | "cross-fade"): void => {
525
- if (!deferredOpen() && inImageValue()) rememberDeferred(refs, name);
465
+ const noteDeferred = (name: "var" | "env" | "cross-fade", imageValue: boolean): void => {
466
+ if (imageValue) rememberDeferred(refs, name);
526
467
  };
527
- const visitor: Visitor<CustomAtRules> = {
528
- Declaration(declaration) {
529
- propertyStack.push(declarationPropertyName(declaration));
530
- },
531
- DeclarationExit() {
532
- propertyStack.pop();
533
- },
534
- Rule: {
535
- "font-face"(rule) {
536
- for (const property of rule.value.properties) {
537
- if (property.type !== "source") continue;
538
- for (const source of property.value) {
539
- if (source.type === "url") fontFaceUrls.push(source.value.url.url);
540
- }
541
- }
542
- },
543
- },
544
- Url(url) {
545
- if (imageFunctionUrlLocations.has(urlLocationKey(url))) return;
546
- if (fontFaceUrls[0] === url.url) {
547
- fontFaceUrls.shift();
548
- return;
468
+
469
+ // image() resolves one source. A discarded non-image argument does not consume it.
470
+ const scanNestedDeferred = (node: CssScanNode): void => {
471
+ if (node.type !== "Function") {
472
+ for (const child of cssChildren(node)) scanNestedDeferred(child);
473
+ return;
474
+ }
475
+ const deferred = deferredFunctionName(node.name ?? "");
476
+ if (deferred) {
477
+ noteDeferred(deferred, true);
478
+ return;
479
+ }
480
+ for (const child of cssChildren(node)) scanNestedDeferred(child);
481
+ };
482
+
483
+ const firstImageSource = (nodes: CssScanNode[]): boolean => {
484
+ let chosen = false;
485
+ for (const node of nodes) {
486
+ if (chosen) {
487
+ scanNestedDeferred(node);
488
+ continue;
549
489
  }
550
- if (deferredOpen()) return;
551
- rememberCssLocator(refs, url.url);
552
- },
553
- Image(image) {
554
- if (deferredOpen()) return;
555
- if (image.type === "image-set") rememberCssImage(refs, image);
556
- },
557
- Token: {
558
- string(token) {
559
- if (deferredOpen()) return;
560
- if (frames.at(-1) !== "image-set") return;
561
- rememberCssLocator(refs, String(token.value));
562
- },
563
- },
564
- Variable() {
565
- noteDeferred("var");
566
- valueDeferDepth++;
567
- },
568
- VariableExit() {
569
- valueDeferDepth--;
570
- },
571
- EnvironmentVariable() {
572
- noteDeferred("env");
573
- valueDeferDepth++;
574
- },
575
- EnvironmentVariableExit() {
576
- valueDeferDepth--;
577
- },
578
- // image() stays unparsed. Quoted URL()/uRl() is a nested function, not argument type "url".
579
- // image-set() with a string or image() option stays unparsed too; its string options are tokens.
580
- // Lowercase var() and env() are not functions; VAR()/ENV() are. cross-fade(), including a vendor prefix, is always a function.
581
- Function(fn) {
582
- const deferred = deferredFunctionName(fn.name);
490
+ if (node.type === "Url" || node.type === "String") {
491
+ if (rememberCssLocator(refs, node.value ?? "")) chosen = true;
492
+ continue;
493
+ }
494
+ if (node.type === "Raw") {
495
+ const parsed = parseValue(node.value ?? "");
496
+ if (parsed && firstImageSource(cssChildren(parsed))) chosen = true;
497
+ continue;
498
+ }
499
+ if (node.type !== "Function") continue;
500
+ const name = (node.name ?? "").toLowerCase();
501
+ const deferred = deferredFunctionName(name);
583
502
  if (deferred) {
584
- noteDeferred(deferred);
585
- frames.push("deferred");
586
- return;
503
+ noteDeferred(deferred, true);
504
+ chosen = true;
505
+ continue;
587
506
  }
588
- if (deferredOpen()) {
589
- frames.push("skip");
590
- return;
507
+ if (name === "type" || name === "format") continue;
508
+ if (firstImageSource(cssChildren(node))) chosen = true;
509
+ }
510
+ return chosen;
511
+ };
512
+
513
+ const imageSetSources = (nodes: CssScanNode[]): void => {
514
+ let option: CssScanNode[] = [];
515
+ const flush = (): void => {
516
+ firstImageSource(option);
517
+ option = [];
518
+ };
519
+ for (const node of nodes) {
520
+ if (node.type === "Operator" && node.value === ",") flush();
521
+ else option.push(node);
522
+ }
523
+ flush();
524
+ };
525
+
526
+ const scanValue = (nodes: CssScanNode[], imageValue: boolean): void => {
527
+ for (const node of nodes) {
528
+ if (node.type === "Url") {
529
+ rememberCssLocator(refs, node.value ?? "");
530
+ continue;
531
+ }
532
+ if (node.type === "Raw") {
533
+ const parsed = parseValue(node.value ?? "");
534
+ if (parsed) scanValue(cssChildren(parsed), imageValue);
535
+ else rawEscapedUrlReferences(node.value ?? "", refs);
536
+ continue;
537
+ }
538
+ if (node.type !== "Function") {
539
+ if (node.children) scanValue(cssChildren(node), imageValue);
540
+ continue;
541
+ }
542
+ const name = (node.name ?? "").toLowerCase();
543
+ const deferred = deferredFunctionName(name);
544
+ if (deferred) {
545
+ noteDeferred(deferred, imageValue);
546
+ continue;
591
547
  }
592
- const name = cssFunctionName(fn.name);
593
548
  if (name === "image") {
594
- if (frames.includes("image")) {
595
- frames.push("skip");
596
- return;
597
- }
598
- frames.push("image");
599
- suppressImageFunctionUrls(fn.arguments, imageFunctionUrlLocations, true);
600
- recordImageFunctionSource(refs, fn.arguments);
601
- return;
549
+ firstImageSource(cssChildren(node));
550
+ continue;
602
551
  }
603
- if ((name === "image-set" || name === "-webkit-image-set") && !frames.includes("image")) {
604
- frames.push("image-set");
605
- suppressNestedFunctionUrls(fn.arguments, imageFunctionUrlLocations);
606
- return;
552
+ if (name === "image-set" || name === "-webkit-image-set") {
553
+ imageSetSources(cssChildren(node));
554
+ continue;
607
555
  }
608
- if (name === "url" && frames.at(-1) === "image-set") {
609
- const locator = quotedUrlFunctionLocator(fn.arguments);
610
- if (locator != null) rememberCssLocator(refs, locator);
556
+ if (name === "url") {
557
+ const quoted = cssChildren(node).find((child) => child.type === "String");
558
+ if (quoted) rememberCssLocator(refs, quoted.value ?? "");
559
+ continue;
611
560
  }
612
- frames.push("skip");
613
- },
614
- FunctionExit() {
615
- frames.pop();
616
- },
561
+ scanValue(cssChildren(node), imageValue);
562
+ }
617
563
  };
618
- const code = textEncoder.encode(css);
619
- try {
620
- if (kind === "declaration") {
621
- transformStyleAttribute({ filename: CSS_FILENAME, code, errorRecovery: true, visitor });
622
- } else {
623
- transform({ filename: CSS_FILENAME, code, errorRecovery: true, visitor });
564
+
565
+ const scanTree = (node: CssScanNode, fontFace = false): void => {
566
+ if (node.type === "Atrule") {
567
+ if (node.block) scanTree(node.block, (node.name ?? "").toLowerCase() === "font-face");
568
+ return;
569
+ }
570
+ if (node.type === "Declaration") {
571
+ const property = (node.property ?? "").toLowerCase();
572
+ if (fontFace && property === "src") return;
573
+ const value = node.value as unknown as CssScanNode | undefined;
574
+ if (value) scanValue(value.type === "Raw" ? [value] : cssChildren(value), IMAGE_VALUE_PROPERTIES.has(property));
575
+ return;
624
576
  }
577
+ if (node.block) scanTree(node.block, fontFace);
578
+ for (const child of cssChildren(node)) scanTree(child, fontFace);
579
+ };
580
+
581
+ try {
582
+ const parsed = parse(css, {
583
+ context: kind === "declaration" ? "declarationList" : "stylesheet",
584
+ list: false,
585
+ }) as unknown as CssScanNode;
586
+ scanTree(parsed);
625
587
  } catch {
626
- return refs;
588
+ // Invalid CSS never causes image inventory to fail open on other artifacts.
627
589
  }
628
590
  return refs;
629
591
  }
@@ -112,13 +112,16 @@ importers:
112
112
 
113
113
  packages/public-runtime:
114
114
  dependencies:
115
- lightningcss:
116
- specifier: ^1.32.0
117
- version: 1.32.0
115
+ css-tree:
116
+ specifier: 2.2.1
117
+ version: 2.2.1
118
118
  parse5:
119
119
  specifier: ^7.2.1
120
120
  version: 7.3.0
121
121
  devDependencies:
122
+ '@types/css-tree':
123
+ specifier: ^3.2.0
124
+ version: 3.2.0
122
125
  ajv:
123
126
  specifier: ^8.20.0
124
127
  version: 8.20.0
@@ -1916,6 +1919,9 @@ packages:
1916
1919
  '@tybys/wasm-util@0.10.3':
1917
1920
  resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
1918
1921
 
1922
+ '@types/css-tree@3.2.0':
1923
+ resolution: {integrity: sha512-J5KXmk6BFIepOT7280FdFyNs4c5fh0Uee0otjKEvzchrRs38Ii9qminqc4ds0L19X8Zd/rT+brR9jkBthygjWw==}
1924
+
1919
1925
  '@types/estree-jsx@1.0.5':
1920
1926
  resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
1921
1927
 
@@ -5201,6 +5207,8 @@ snapshots:
5201
5207
  tslib: 2.8.1
5202
5208
  optional: true
5203
5209
 
5210
+ '@types/css-tree@3.2.0': {}
5211
+
5204
5212
  '@types/estree-jsx@1.0.5':
5205
5213
  dependencies:
5206
5214
  '@types/estree': 1.0.9