@tokenoftrust/storefront-runner 2.0.50 → 2.0.52

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.50",
3
+ "version": "2.0.52",
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"
@@ -0,0 +1,3 @@
1
+ declare module "css-tree/dist/csstree.esm" {
2
+ export { parse, tokenize, tokenNames, ident, string, url } from "css-tree";
3
+ }
@@ -1,4 +1,5 @@
1
- import { transform, transformStyleAttribute, type CustomAtRules, type Declaration, type Image, type TokenOrValue, type Url, type Visitor } from "lightningcss";
1
+ /// <reference path="./css-tree-worker.d.ts" />
2
+ import { ident, parse, string as cssString, tokenize, tokenNames, url as cssUrl } from "css-tree/dist/csstree.esm";
2
3
  import { parseFragment, type DefaultTreeAdapterMap } from "parse5";
3
4
 
4
5
  /**
@@ -394,64 +395,46 @@ function rememberCssLocator(refs: ExtractedReference[], locator: string): boolea
394
395
  return true;
395
396
  }
396
397
 
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;
398
+ type CssScanNode = {
399
+ type: string;
400
+ name?: string;
401
+ value?: string;
402
+ property?: string;
403
+ children?: CssScanNode[];
404
+ block?: CssScanNode | null;
405
+ };
406
+
407
+ function cssChildren(node: CssScanNode): CssScanNode[] {
408
+ return node.children ?? [];
409
+ }
410
+
411
+ function rawEscapedUrlReferences(raw: string, refs: ExtractedReference[]): void {
412
+ const tokens: Array<{ name: string; start: number; end: number }> = [];
413
+ tokenize(raw, (type, start, end) => tokens.push({ name: tokenNames[type] ?? "", start, end }));
414
+ for (let index = 0; index < tokens.length; index++) {
415
+ const token = tokens[index]!;
416
+ const rawName = raw.slice(token.start, token.end - 1);
417
+ if (token.name !== "function-token" || !rawName.includes("\\") || ident.decode(rawName).toLowerCase() !== "url") continue;
418
+ let depth = 1;
419
+ for (let next = index + 1; next < tokens.length; next++) {
420
+ const nextToken = tokens[next]!;
421
+ if (nextToken.name === "function-token" || nextToken.name === "(-token") depth++;
422
+ if (nextToken.name === ")-token") depth--;
423
+ if (depth !== 0) continue;
424
+ const argument = raw.slice(token.end, nextToken.start).trim();
425
+ const locator = argument.startsWith('"') || argument.startsWith("'")
426
+ ? cssString.decode(argument)
427
+ : cssUrl.decode(`url(${argument})`);
428
+ rememberCssLocator(refs, locator);
429
+ index = next;
430
+ break;
436
431
  }
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
432
  }
449
433
  }
450
434
 
451
435
  function deferredFunctionName(name: string): "var" | "env" | "cross-fade" | null {
452
- const normalized = cssFunctionName(name);
436
+ const normalized = name.toLowerCase();
453
437
  if (normalized === "var" || normalized === "env") return normalized;
454
- // -webkit-cross-fade is cross-fade. Any vendor prefix stays unresolved.
455
438
  if (normalized === "cross-fade" || /^-[a-z]+-cross-fade$/.test(normalized)) return "cross-fade";
456
439
  return null;
457
440
  }
@@ -460,170 +443,150 @@ function rememberDeferred(refs: ExtractedReference[], name: "var" | "env" | "cro
460
443
  refs.push({ locator: `css-function:${name}`, surface: "css-url", deferredFunction: name });
461
444
  }
462
445
 
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
446
  function adoptCssReferences(refs: ExtractedReference[], cssRefs: readonly ExtractedReference[], afterBase: boolean): void {
492
447
  for (const ref of cssRefs) refs.push(afterBase ? { ...ref, afterBase: true } : ref);
493
448
  }
494
449
 
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
-
450
+ /**
451
+ * CSS extraction uses a JavaScript parser because this module is imported by
452
+ * Worker routes. A native CSS binding cannot be evaluated by workerd, which
453
+ * would prevent every route sharing its bundle chunk from loading.
454
+ */
504
455
  function cssReferences(css: string, kind: "stylesheet" | "declaration"): ExtractedReference[] {
505
456
  if (!css) return [];
506
457
  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);
458
+ const parseValue = (value: string): CssScanNode | null => {
459
+ try {
460
+ const parsed = parse(value, { context: "value", list: false }) as unknown as CssScanNode;
461
+ return parsed.type === "Value" ? parsed : null;
462
+ } catch {
463
+ return null;
464
+ }
523
465
  };
524
- const noteDeferred = (name: "var" | "env" | "cross-fade"): void => {
525
- if (!deferredOpen() && inImageValue()) rememberDeferred(refs, name);
466
+ const noteDeferred = (name: "var" | "env" | "cross-fade", imageValue: boolean): void => {
467
+ if (imageValue) rememberDeferred(refs, name);
526
468
  };
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;
469
+
470
+ // image() resolves one source. A discarded non-image argument does not consume it.
471
+ const scanNestedDeferred = (node: CssScanNode): void => {
472
+ if (node.type !== "Function") {
473
+ for (const child of cssChildren(node)) scanNestedDeferred(child);
474
+ return;
475
+ }
476
+ const deferred = deferredFunctionName(node.name ?? "");
477
+ if (deferred) {
478
+ noteDeferred(deferred, true);
479
+ return;
480
+ }
481
+ for (const child of cssChildren(node)) scanNestedDeferred(child);
482
+ };
483
+
484
+ const firstImageSource = (nodes: CssScanNode[]): boolean => {
485
+ let chosen = false;
486
+ for (const node of nodes) {
487
+ if (chosen) {
488
+ scanNestedDeferred(node);
489
+ continue;
549
490
  }
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);
491
+ if (node.type === "Url" || node.type === "String") {
492
+ if (rememberCssLocator(refs, node.value ?? "")) chosen = true;
493
+ continue;
494
+ }
495
+ if (node.type === "Raw") {
496
+ const parsed = parseValue(node.value ?? "");
497
+ if (parsed && firstImageSource(cssChildren(parsed))) chosen = true;
498
+ continue;
499
+ }
500
+ if (node.type !== "Function") continue;
501
+ const name = (node.name ?? "").toLowerCase();
502
+ const deferred = deferredFunctionName(name);
583
503
  if (deferred) {
584
- noteDeferred(deferred);
585
- frames.push("deferred");
586
- return;
504
+ noteDeferred(deferred, true);
505
+ chosen = true;
506
+ continue;
587
507
  }
588
- if (deferredOpen()) {
589
- frames.push("skip");
590
- return;
508
+ if (name === "type" || name === "format") continue;
509
+ if (firstImageSource(cssChildren(node))) chosen = true;
510
+ }
511
+ return chosen;
512
+ };
513
+
514
+ const imageSetSources = (nodes: CssScanNode[]): void => {
515
+ let option: CssScanNode[] = [];
516
+ const flush = (): void => {
517
+ firstImageSource(option);
518
+ option = [];
519
+ };
520
+ for (const node of nodes) {
521
+ if (node.type === "Operator" && node.value === ",") flush();
522
+ else option.push(node);
523
+ }
524
+ flush();
525
+ };
526
+
527
+ const scanValue = (nodes: CssScanNode[], imageValue: boolean): void => {
528
+ for (const node of nodes) {
529
+ if (node.type === "Url") {
530
+ rememberCssLocator(refs, node.value ?? "");
531
+ continue;
532
+ }
533
+ if (node.type === "Raw") {
534
+ const parsed = parseValue(node.value ?? "");
535
+ if (parsed) scanValue(cssChildren(parsed), imageValue);
536
+ else rawEscapedUrlReferences(node.value ?? "", refs);
537
+ continue;
538
+ }
539
+ if (node.type !== "Function") {
540
+ if (node.children) scanValue(cssChildren(node), imageValue);
541
+ continue;
542
+ }
543
+ const name = (node.name ?? "").toLowerCase();
544
+ const deferred = deferredFunctionName(name);
545
+ if (deferred) {
546
+ noteDeferred(deferred, imageValue);
547
+ continue;
591
548
  }
592
- const name = cssFunctionName(fn.name);
593
549
  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;
550
+ firstImageSource(cssChildren(node));
551
+ continue;
602
552
  }
603
- if ((name === "image-set" || name === "-webkit-image-set") && !frames.includes("image")) {
604
- frames.push("image-set");
605
- suppressNestedFunctionUrls(fn.arguments, imageFunctionUrlLocations);
606
- return;
553
+ if (name === "image-set" || name === "-webkit-image-set") {
554
+ imageSetSources(cssChildren(node));
555
+ continue;
607
556
  }
608
- if (name === "url" && frames.at(-1) === "image-set") {
609
- const locator = quotedUrlFunctionLocator(fn.arguments);
610
- if (locator != null) rememberCssLocator(refs, locator);
557
+ if (name === "url") {
558
+ const quoted = cssChildren(node).find((child) => child.type === "String");
559
+ if (quoted) rememberCssLocator(refs, quoted.value ?? "");
560
+ continue;
611
561
  }
612
- frames.push("skip");
613
- },
614
- FunctionExit() {
615
- frames.pop();
616
- },
562
+ scanValue(cssChildren(node), imageValue);
563
+ }
617
564
  };
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 });
565
+
566
+ const scanTree = (node: CssScanNode, fontFace = false): void => {
567
+ if (node.type === "Atrule") {
568
+ if (node.block) scanTree(node.block, (node.name ?? "").toLowerCase() === "font-face");
569
+ return;
570
+ }
571
+ if (node.type === "Declaration") {
572
+ const property = (node.property ?? "").toLowerCase();
573
+ if (fontFace && property === "src") return;
574
+ const value = node.value as unknown as CssScanNode | undefined;
575
+ if (value) scanValue(value.type === "Raw" ? [value] : cssChildren(value), IMAGE_VALUE_PROPERTIES.has(property));
576
+ return;
624
577
  }
578
+ if (node.block) scanTree(node.block, fontFace);
579
+ for (const child of cssChildren(node)) scanTree(child, fontFace);
580
+ };
581
+
582
+ try {
583
+ const parsed = parse(css, {
584
+ context: kind === "declaration" ? "declarationList" : "stylesheet",
585
+ list: false,
586
+ }) as unknown as CssScanNode;
587
+ scanTree(parsed);
625
588
  } catch {
626
- return refs;
589
+ // Invalid CSS never causes image inventory to fail open on other artifacts.
627
590
  }
628
591
  return refs;
629
592
  }
@@ -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