td-ai-tools 1.2.11 → 1.3.2
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 +1 -1
- package/skills/README.md +2 -2
- package/skills/shopify-lint/SKILL.md +26 -18
- package/skills/shopify-lint/eslint-plugin-theory/README.md +25 -8
- package/skills/shopify-lint/eslint-plugin-theory/src/liquid/mask.test.ts +41 -0
- package/skills/shopify-lint/eslint-plugin-theory/src/liquid/mask.ts +41 -2
- package/skills/shopify-lint/hooks/claude-stop.sh +2 -3
- package/skills/shopify-lint/hooks/codex-stop.sh +2 -3
- package/skills/shopify-lint/scripts/__pycache__/shopify_lint.cpython-312.pyc +0 -0
- package/skills/shopify-lint/scripts/setup.sh +21 -2
- package/skills/shopify-lint/scripts/shopify_lint.py +179 -30
- package/skills/shopify-lint/stylelint-config-theory/.stylelintignore +9 -0
- package/skills/shopify-lint/stylelint-config-theory/README.md +144 -0
- package/skills/shopify-lint/stylelint-config-theory/bin/lint-css.mjs +206 -0
- package/skills/shopify-lint/stylelint-config-theory/package-lock.json +3110 -0
- package/skills/shopify-lint/stylelint-config-theory/package.json +47 -0
- package/skills/shopify-lint/stylelint-config-theory/src/build.test.ts +31 -0
- package/skills/shopify-lint/stylelint-config-theory/src/config.test.ts +150 -0
- package/skills/shopify-lint/stylelint-config-theory/src/config.ts +109 -0
- package/skills/shopify-lint/stylelint-config-theory/src/index.ts +15 -0
- package/skills/shopify-lint/stylelint-config-theory/src/integration.test.ts +187 -0
- package/skills/shopify-lint/stylelint-config-theory/src/liquid/mask.test.ts +300 -0
- package/skills/shopify-lint/stylelint-config-theory/src/liquid/mask.ts +203 -0
- package/skills/shopify-lint/stylelint-config-theory/src/types/shared-configs.d.ts +26 -0
- package/skills/shopify-lint/stylelint-config-theory/tsconfig.build.json +9 -0
- package/skills/shopify-lint/stylelint-config-theory/tsconfig.json +14 -0
- package/skills/shopify-lint/stylelint-config-theory/vitest.config.ts +11 -0
- package/skills/shopify-lint/tests/__pycache__/test_shopify_lint.cpython-312.pyc +0 -0
- package/skills/shopify-lint/tests/test_shopify_lint.py +483 -8
- package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +7 -0
- package/skills/shopify-lint/theme-check-theory/README.md +48 -0
- package/skills/shopify-pre-pr/SKILL.md +129 -0
- package/skills/shopify-pre-pr/agents/openai.yaml +4 -0
- package/skills/shopify-pre-pr/references/td-change-delimiters.md +88 -0
- package/skills/shopify-pre-pr/scripts/__pycache__/test_vendor_change_audit.cpython-312.pyc +0 -0
- package/skills/shopify-pre-pr/scripts/__pycache__/vendor_change_audit.cpython-312.pyc +0 -0
- package/skills/shopify-pre-pr/scripts/test_vendor_change_audit.py +424 -0
- package/skills/shopify-pre-pr/scripts/vendor_change_audit.py +540 -0
- package/skills/record-changes/SKILL.md +0 -76
- package/skills/record-changes/agents/openai.yaml +0 -4
- /package/skills/{record-changes → shopify-pre-pr}/scripts/branch_diff_context.py +0 -0
|
@@ -135,14 +135,14 @@ class ThemeCheckTests(unittest.TestCase):
|
|
|
135
135
|
def test_advisory_offenses_never_fail_at_the_level_the_hooks_use(self) -> None:
|
|
136
136
|
"""The Stop hooks run at --fail-level warning; info must stay advisory.
|
|
137
137
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
Nothing this skill reports is graded `info` today — Theme Check can be
|
|
139
|
+
configured to, and the level exists for a check whose findings should
|
|
140
|
+
surface without ever blocking handoff.
|
|
141
141
|
"""
|
|
142
142
|
info_report = [
|
|
143
143
|
{
|
|
144
144
|
"path": "changed.liquid",
|
|
145
|
-
"offenses": [{"check": "
|
|
145
|
+
"offenses": [{"check": "SomeAdvisoryCheck", "severity": "info"}],
|
|
146
146
|
}
|
|
147
147
|
]
|
|
148
148
|
|
|
@@ -439,7 +439,14 @@ class EslintNormalizationTests(unittest.TestCase):
|
|
|
439
439
|
self.assertEqual(offense["severity"], "error")
|
|
440
440
|
self.assertNotIn("masked", offense["message"])
|
|
441
441
|
|
|
442
|
-
def
|
|
442
|
+
def test_parse_error_inside_liquid_is_an_error_too(self) -> None:
|
|
443
|
+
"""A `{% javascript %}` parse error is graded like any other.
|
|
444
|
+
|
|
445
|
+
It used to be demoted to `info`, because Liquid that straddles a
|
|
446
|
+
JavaScript syntax boundary cannot be masked into parseable code. Liquid
|
|
447
|
+
in the block is now reported by `StaticStylesheetAndJavascriptTags`, so the
|
|
448
|
+
fix is well defined and the parse error blocks like the rest.
|
|
449
|
+
"""
|
|
443
450
|
results = [
|
|
444
451
|
{
|
|
445
452
|
"filePath": "/repo/sections/td-hero.liquid",
|
|
@@ -459,12 +466,397 @@ class EslintNormalizationTests(unittest.TestCase):
|
|
|
459
466
|
[report] = shopify_lint.normalize_eslint_results(results)
|
|
460
467
|
offense = report["offenses"][0]
|
|
461
468
|
|
|
462
|
-
self.assertEqual(offense["
|
|
463
|
-
self.
|
|
464
|
-
self.assertEqual(
|
|
469
|
+
self.assertEqual(offense["check"], "eslint/parse-error")
|
|
470
|
+
self.assertEqual(offense["severity"], "error")
|
|
471
|
+
self.assertEqual(offense["message"], "Parsing error: Unexpected token")
|
|
472
|
+
self.assertEqual(shopify_lint.exit_code_for([report], "error"), 1)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class CssTargetTests(unittest.TestCase):
|
|
476
|
+
def build_theme(self, root: Path, names: tuple[str, ...]) -> None:
|
|
477
|
+
for name in names:
|
|
478
|
+
path = root / name
|
|
479
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
480
|
+
path.write_text("content\n")
|
|
481
|
+
|
|
482
|
+
def test_selects_css_and_liquid_files_only(self) -> None:
|
|
483
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
484
|
+
root = Path(directory)
|
|
485
|
+
names = (
|
|
486
|
+
"assets/component-td-card.css",
|
|
487
|
+
"sections/td-hero.liquid",
|
|
488
|
+
"assets/td-app.js",
|
|
489
|
+
"config/settings_schema.json",
|
|
490
|
+
)
|
|
491
|
+
self.build_theme(root, names)
|
|
492
|
+
|
|
493
|
+
targets = shopify_lint.select_css_targets(set(names), root, root)
|
|
494
|
+
|
|
495
|
+
self.assertEqual(
|
|
496
|
+
[target.relative_to(root).as_posix() for target in targets],
|
|
497
|
+
["assets/component-td-card.css", "sections/td-hero.liquid"],
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
def test_skips_css_without_the_theory_prefix(self) -> None:
|
|
501
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
502
|
+
root = Path(directory)
|
|
503
|
+
names = (
|
|
504
|
+
"assets/component-td-card.css",
|
|
505
|
+
"assets/vendor.css",
|
|
506
|
+
"assets/theme.css",
|
|
507
|
+
)
|
|
508
|
+
self.build_theme(root, names)
|
|
509
|
+
|
|
510
|
+
targets = shopify_lint.select_css_targets(set(names), root, root)
|
|
511
|
+
|
|
512
|
+
self.assertEqual(
|
|
513
|
+
[target.relative_to(root).as_posix() for target in targets],
|
|
514
|
+
["assets/component-td-card.css"],
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def test_prefix_match_is_case_insensitive_and_positional(self) -> None:
|
|
518
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
519
|
+
root = Path(directory)
|
|
520
|
+
names = (
|
|
521
|
+
"assets/TD-Hero.css",
|
|
522
|
+
"assets/section-td-banner.css",
|
|
523
|
+
"assets/tdcard.css",
|
|
524
|
+
"assets/standard.css",
|
|
525
|
+
)
|
|
526
|
+
self.build_theme(root, names)
|
|
527
|
+
|
|
528
|
+
targets = shopify_lint.select_css_targets(set(names), root, root)
|
|
529
|
+
|
|
530
|
+
self.assertEqual(
|
|
531
|
+
[target.relative_to(root).as_posix() for target in targets],
|
|
532
|
+
["assets/TD-Hero.css", "assets/section-td-banner.css"],
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
def test_prefix_is_matched_on_the_filename_not_the_directory(self) -> None:
|
|
536
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
537
|
+
root = Path(directory)
|
|
538
|
+
names = ("td-components/vendor.css", "assets/component-td-card.css")
|
|
539
|
+
self.build_theme(root, names)
|
|
540
|
+
|
|
541
|
+
targets = shopify_lint.select_css_targets(set(names), root, root)
|
|
542
|
+
|
|
543
|
+
self.assertEqual(
|
|
544
|
+
[target.relative_to(root).as_posix() for target in targets],
|
|
545
|
+
["assets/component-td-card.css"],
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
def test_liquid_is_linted_regardless_of_its_name(self) -> None:
|
|
549
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
550
|
+
root = Path(directory)
|
|
551
|
+
names = ("sections/main-product.liquid", "snippets/vendor-widget.liquid")
|
|
552
|
+
self.build_theme(root, names)
|
|
553
|
+
|
|
554
|
+
targets = shopify_lint.select_css_targets(set(names), root, root)
|
|
555
|
+
|
|
556
|
+
self.assertEqual(
|
|
557
|
+
[target.relative_to(root).as_posix() for target in targets],
|
|
558
|
+
["sections/main-product.liquid", "snippets/vendor-widget.liquid"],
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
def test_skips_minified_bundles_even_with_the_theory_prefix(self) -> None:
|
|
562
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
563
|
+
root = Path(directory)
|
|
564
|
+
names = ("assets/td-vendor.min.css", "assets/component-td-card.css")
|
|
565
|
+
self.build_theme(root, names)
|
|
566
|
+
|
|
567
|
+
targets = shopify_lint.select_css_targets(set(names), root, root)
|
|
568
|
+
|
|
569
|
+
self.assertEqual(
|
|
570
|
+
[target.relative_to(root).as_posix() for target in targets],
|
|
571
|
+
["assets/component-td-card.css"],
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
def test_skips_paths_that_no_longer_exist(self) -> None:
|
|
575
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
576
|
+
root = Path(directory)
|
|
577
|
+
self.build_theme(root, ("assets/td-kept.css",))
|
|
578
|
+
|
|
579
|
+
targets = shopify_lint.select_css_targets(
|
|
580
|
+
{"assets/td-kept.css", "assets/td-deleted.css"}, root, root
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
self.assertEqual(
|
|
584
|
+
[target.relative_to(root).as_posix() for target in targets],
|
|
585
|
+
["assets/td-kept.css"],
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
def test_skips_paths_outside_the_theme_root(self) -> None:
|
|
589
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
590
|
+
root = Path(directory)
|
|
591
|
+
# Both carry the Theory prefix, so only the theme-root check can
|
|
592
|
+
# be what excludes the second one.
|
|
593
|
+
self.build_theme(root, ("theme/assets/td-app.css", "tooling/td-docs.css"))
|
|
594
|
+
|
|
595
|
+
targets = shopify_lint.select_css_targets(
|
|
596
|
+
{"theme/assets/td-app.css", "tooling/td-docs.css"}, root, root / "theme"
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
self.assertEqual(
|
|
600
|
+
[target.relative_to(root).as_posix() for target in targets],
|
|
601
|
+
["theme/assets/td-app.css"],
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class CssLintCommandTests(unittest.TestCase):
|
|
606
|
+
def test_does_not_spawn_node_when_there_are_no_targets(self) -> None:
|
|
607
|
+
with patch.object(shopify_lint.subprocess, "run") as run:
|
|
608
|
+
result = shopify_lint.run_css_lint(Path("/theme"), [])
|
|
609
|
+
|
|
610
|
+
self.assertEqual(result, [])
|
|
611
|
+
run.assert_not_called()
|
|
612
|
+
|
|
613
|
+
def test_runs_the_bundled_runner_with_a_json_payload(self) -> None:
|
|
614
|
+
completed = subprocess.CompletedProcess([], 0, stdout="[]", stderr="")
|
|
615
|
+
runner = Path("/skill/stylelint-config-theory/bin/lint-css.mjs")
|
|
616
|
+
|
|
617
|
+
with patch.object(shopify_lint.Path, "is_file", return_value=True):
|
|
618
|
+
with patch.object(
|
|
619
|
+
shopify_lint.subprocess, "run", return_value=completed
|
|
620
|
+
) as run:
|
|
621
|
+
result = shopify_lint.run_css_lint(
|
|
622
|
+
Path("/theme"), [Path("/theme/assets/td-app.css")], runner
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
self.assertEqual(result, [])
|
|
626
|
+
run.assert_called_once_with(
|
|
627
|
+
["node", str(runner)],
|
|
628
|
+
input=json.dumps(
|
|
629
|
+
{"cwd": "/theme", "files": ["/theme/assets/td-app.css"]}
|
|
630
|
+
),
|
|
631
|
+
cwd=Path("/theme"),
|
|
632
|
+
capture_output=True,
|
|
633
|
+
text=True,
|
|
634
|
+
check=False,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
def test_missing_runner_asks_for_setup(self) -> None:
|
|
638
|
+
with self.assertRaises(shopify_lint.ShopifyLintError) as error:
|
|
639
|
+
shopify_lint.run_css_lint(
|
|
640
|
+
Path("/theme"),
|
|
641
|
+
[Path("/theme/assets/td-app.css")],
|
|
642
|
+
Path("/nowhere/lint-css.mjs"),
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
self.assertIn("setup.sh", str(error.exception))
|
|
646
|
+
|
|
647
|
+
def test_missing_node_is_reported(self) -> None:
|
|
648
|
+
with patch.object(shopify_lint.Path, "is_file", return_value=True):
|
|
649
|
+
with patch.object(
|
|
650
|
+
shopify_lint.subprocess, "run", side_effect=FileNotFoundError
|
|
651
|
+
):
|
|
652
|
+
with self.assertRaises(shopify_lint.ShopifyLintError) as error:
|
|
653
|
+
shopify_lint.run_css_lint(
|
|
654
|
+
Path("/theme"), [Path("/theme/assets/td-app.css")]
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
self.assertIn("Node.js", str(error.exception))
|
|
658
|
+
|
|
659
|
+
def test_runner_failure_is_reported(self) -> None:
|
|
660
|
+
completed = subprocess.CompletedProcess(
|
|
661
|
+
[], 1, stdout="", stderr="theory css lint: config is invalid"
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
with patch.object(shopify_lint.Path, "is_file", return_value=True):
|
|
665
|
+
with patch.object(shopify_lint.subprocess, "run", return_value=completed):
|
|
666
|
+
with self.assertRaises(shopify_lint.ShopifyLintError) as error:
|
|
667
|
+
shopify_lint.run_css_lint(
|
|
668
|
+
Path("/theme"), [Path("/theme/assets/td-app.css")]
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
self.assertIn("config is invalid", str(error.exception))
|
|
672
|
+
|
|
673
|
+
def test_non_json_output_is_reported(self) -> None:
|
|
674
|
+
completed = subprocess.CompletedProcess([], 0, stdout="not json", stderr="")
|
|
675
|
+
|
|
676
|
+
with patch.object(shopify_lint.Path, "is_file", return_value=True):
|
|
677
|
+
with patch.object(shopify_lint.subprocess, "run", return_value=completed):
|
|
678
|
+
with self.assertRaises(shopify_lint.ShopifyLintError) as error:
|
|
679
|
+
shopify_lint.run_css_lint(
|
|
680
|
+
Path("/theme"), [Path("/theme/assets/td-app.css")]
|
|
681
|
+
)
|
|
682
|
+
|
|
683
|
+
self.assertIn("Theory CSS lint failed", str(error.exception))
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
class StylelintNormalizationTests(unittest.TestCase):
|
|
687
|
+
def test_namespaces_rules_and_zero_indexes_positions(self) -> None:
|
|
688
|
+
results = [
|
|
689
|
+
{
|
|
690
|
+
"path": "/theme/assets/component-td-card.css",
|
|
691
|
+
"warnings": [
|
|
692
|
+
{
|
|
693
|
+
"line": 4,
|
|
694
|
+
"column": 3,
|
|
695
|
+
"rule": "length-zero-no-unit",
|
|
696
|
+
"severity": "warning",
|
|
697
|
+
"text": "Disallowed unit (length-zero-no-unit)",
|
|
698
|
+
}
|
|
699
|
+
],
|
|
700
|
+
}
|
|
701
|
+
]
|
|
702
|
+
|
|
703
|
+
[report] = shopify_lint.normalize_stylelint_results(results)
|
|
704
|
+
offense = report["offenses"][0]
|
|
705
|
+
|
|
706
|
+
self.assertEqual(offense["check"], "stylelint/length-zero-no-unit")
|
|
707
|
+
self.assertEqual(offense["severity"], "warning")
|
|
708
|
+
self.assertEqual(offense["start_row"], 3)
|
|
709
|
+
self.assertEqual(offense["start_column"], 2)
|
|
710
|
+
|
|
711
|
+
def test_keeps_a_namespaced_plugin_rule_as_is(self) -> None:
|
|
712
|
+
results = [
|
|
713
|
+
{
|
|
714
|
+
"path": "/theme/assets/component-td-card.css",
|
|
715
|
+
"warnings": [
|
|
716
|
+
{"line": 1, "column": 1, "rule": "theory/no-raw-colors", "severity": "error"}
|
|
717
|
+
],
|
|
718
|
+
}
|
|
719
|
+
]
|
|
720
|
+
|
|
721
|
+
[report] = shopify_lint.normalize_stylelint_results(results)
|
|
722
|
+
|
|
723
|
+
self.assertEqual(report["offenses"][0]["check"], "theory/no-raw-colors")
|
|
724
|
+
|
|
725
|
+
def test_drops_files_without_warnings(self) -> None:
|
|
726
|
+
results = [{"path": "/theme/assets/component-td-card.css", "warnings": []}]
|
|
727
|
+
|
|
728
|
+
self.assertEqual(shopify_lint.normalize_stylelint_results(results), [])
|
|
729
|
+
|
|
730
|
+
def test_css_parse_error_is_an_error(self) -> None:
|
|
731
|
+
results = [
|
|
732
|
+
{
|
|
733
|
+
"path": "/theme/assets/component-td-card.css",
|
|
734
|
+
"warnings": [
|
|
735
|
+
{
|
|
736
|
+
"line": 1,
|
|
737
|
+
"column": 1,
|
|
738
|
+
"rule": "CssSyntaxError",
|
|
739
|
+
"severity": "error",
|
|
740
|
+
"text": "Unclosed block (CssSyntaxError)",
|
|
741
|
+
}
|
|
742
|
+
],
|
|
743
|
+
}
|
|
744
|
+
]
|
|
745
|
+
|
|
746
|
+
[report] = shopify_lint.normalize_stylelint_results(results)
|
|
747
|
+
offense = report["offenses"][0]
|
|
748
|
+
|
|
749
|
+
self.assertEqual(offense["check"], "stylelint/parse-error")
|
|
750
|
+
self.assertEqual(offense["severity"], "error")
|
|
751
|
+
self.assertNotIn("masked before parsing", offense["message"])
|
|
752
|
+
|
|
753
|
+
def test_liquid_parse_error_is_an_error_too(self) -> None:
|
|
754
|
+
"""A `{% stylesheet %}` parse error is graded like any other.
|
|
755
|
+
|
|
756
|
+
It used to be demoted to `info` because masking Liquid out of the block
|
|
757
|
+
cannot always produce parseable CSS. Liquid in the block is now reported
|
|
758
|
+
by `StaticStylesheetAndJavascriptTags`, so the fix is well defined.
|
|
759
|
+
"""
|
|
760
|
+
results = [
|
|
761
|
+
{
|
|
762
|
+
"path": "/theme/sections/td-hero.liquid",
|
|
763
|
+
"warnings": [
|
|
764
|
+
{
|
|
765
|
+
"line": 3,
|
|
766
|
+
"column": 1,
|
|
767
|
+
"rule": "CssSyntaxError",
|
|
768
|
+
"severity": "error",
|
|
769
|
+
"text": "Unclosed block (CssSyntaxError)",
|
|
770
|
+
}
|
|
771
|
+
],
|
|
772
|
+
}
|
|
773
|
+
]
|
|
774
|
+
|
|
775
|
+
[report] = shopify_lint.normalize_stylelint_results(results)
|
|
776
|
+
offense = report["offenses"][0]
|
|
777
|
+
|
|
778
|
+
self.assertEqual(offense["check"], "stylelint/parse-error")
|
|
779
|
+
self.assertEqual(offense["severity"], "error")
|
|
780
|
+
self.assertEqual(offense["message"], "Unclosed block (CssSyntaxError)")
|
|
781
|
+
self.assertEqual(shopify_lint.exit_code_for([report], "error"), 1)
|
|
782
|
+
|
|
783
|
+
def test_a_rule_offense_in_liquid_keeps_its_severity(self) -> None:
|
|
784
|
+
# A rule offense inside a `{% stylesheet %}` block is graded exactly as
|
|
785
|
+
# one in a standalone `.css` file, and must still block a handoff.
|
|
786
|
+
results = [
|
|
787
|
+
{
|
|
788
|
+
"path": "/theme/sections/td-hero.liquid",
|
|
789
|
+
"warnings": [
|
|
790
|
+
{
|
|
791
|
+
"line": 3,
|
|
792
|
+
"column": 1,
|
|
793
|
+
"rule": "block-no-empty",
|
|
794
|
+
"severity": "error",
|
|
795
|
+
"text": "Empty block (block-no-empty)",
|
|
796
|
+
}
|
|
797
|
+
],
|
|
798
|
+
}
|
|
799
|
+
]
|
|
800
|
+
|
|
801
|
+
[report] = shopify_lint.normalize_stylelint_results(results)
|
|
802
|
+
|
|
803
|
+
self.assertEqual(report["offenses"][0]["severity"], "error")
|
|
804
|
+
self.assertEqual(shopify_lint.exit_code_for([report], "error"), 1)
|
|
805
|
+
|
|
806
|
+
def test_an_unrecognized_severity_falls_back_to_warning(self) -> None:
|
|
807
|
+
results = [
|
|
808
|
+
{
|
|
809
|
+
"path": "/theme/assets/td-app.css",
|
|
810
|
+
"warnings": [{"line": 1, "column": 1, "rule": "block-no-empty"}],
|
|
811
|
+
}
|
|
812
|
+
]
|
|
813
|
+
|
|
814
|
+
[report] = shopify_lint.normalize_stylelint_results(results)
|
|
815
|
+
|
|
816
|
+
self.assertEqual(report["offenses"][0]["severity"], "warning")
|
|
465
817
|
|
|
466
818
|
|
|
467
819
|
class MergeReportTests(unittest.TestCase):
|
|
820
|
+
def test_merges_all_three_streams_for_one_file(self) -> None:
|
|
821
|
+
theme_reports = [
|
|
822
|
+
{
|
|
823
|
+
"path": "/repo/sections/td-hero.liquid",
|
|
824
|
+
"offenses": [
|
|
825
|
+
{"check": "HardcodedText", "severity": "warning", "start_row": 12}
|
|
826
|
+
],
|
|
827
|
+
}
|
|
828
|
+
]
|
|
829
|
+
js_reports = [
|
|
830
|
+
{
|
|
831
|
+
"path": "/repo/sections/td-hero.liquid",
|
|
832
|
+
"offenses": [
|
|
833
|
+
{"check": "eslint/no-dupe-keys", "severity": "error", "start_row": 3}
|
|
834
|
+
],
|
|
835
|
+
}
|
|
836
|
+
]
|
|
837
|
+
css_reports = [
|
|
838
|
+
{
|
|
839
|
+
"path": "/repo/sections/td-hero.liquid",
|
|
840
|
+
"offenses": [
|
|
841
|
+
{
|
|
842
|
+
"check": "stylelint/block-no-empty",
|
|
843
|
+
"severity": "error",
|
|
844
|
+
"start_row": 7,
|
|
845
|
+
}
|
|
846
|
+
],
|
|
847
|
+
}
|
|
848
|
+
]
|
|
849
|
+
|
|
850
|
+
merged = shopify_lint.merge_reports(
|
|
851
|
+
theme_reports, js_reports, css_reports, repo_root=Path("/repo")
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
self.assertEqual(len(merged), 1)
|
|
855
|
+
self.assertEqual(
|
|
856
|
+
[offense["check"] for offense in merged[0]["offenses"]],
|
|
857
|
+
["eslint/no-dupe-keys", "stylelint/block-no-empty", "HardcodedText"],
|
|
858
|
+
)
|
|
859
|
+
|
|
468
860
|
def test_merges_both_streams_for_one_file_and_sorts_offenses(self) -> None:
|
|
469
861
|
theme_reports = [
|
|
470
862
|
{
|
|
@@ -582,5 +974,88 @@ class EndToEndJsTests(unittest.TestCase):
|
|
|
582
974
|
self.assertEqual(dupe[0]["severity"], "error")
|
|
583
975
|
|
|
584
976
|
|
|
977
|
+
@unittest.skipUnless(
|
|
978
|
+
(SKILL_ROOT / "stylelint-config-theory" / "dist").is_dir(),
|
|
979
|
+
"run scripts/setup.sh to build stylelint-config-theory",
|
|
980
|
+
)
|
|
981
|
+
class EndToEndCssTests(unittest.TestCase):
|
|
982
|
+
"""Proves the Liquid line-mapping contract against the real CSS runner."""
|
|
983
|
+
|
|
984
|
+
def test_reports_stylesheet_block_offenses_at_their_real_line(self) -> None:
|
|
985
|
+
lines = [
|
|
986
|
+
'<div class="td-hero">',
|
|
987
|
+
" <h2>{{ section.settings.heading }}</h2>",
|
|
988
|
+
"</div>",
|
|
989
|
+
"",
|
|
990
|
+
"{% stylesheet %}",
|
|
991
|
+
" .td-hero {",
|
|
992
|
+
" margin: {{ section.settings.gap }}px;",
|
|
993
|
+
" }",
|
|
994
|
+
" .td-broken {}",
|
|
995
|
+
"{% endstylesheet %}",
|
|
996
|
+
]
|
|
997
|
+
expected_row = lines.index(" .td-broken {}")
|
|
998
|
+
|
|
999
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
1000
|
+
root = Path(directory)
|
|
1001
|
+
section = root / "sections" / "td-hero.liquid"
|
|
1002
|
+
section.parent.mkdir(parents=True)
|
|
1003
|
+
section.write_text("\n".join(lines) + "\n")
|
|
1004
|
+
|
|
1005
|
+
results = shopify_lint.run_css_lint(root, [section])
|
|
1006
|
+
[report] = shopify_lint.normalize_stylelint_results(results)
|
|
1007
|
+
|
|
1008
|
+
empty = [
|
|
1009
|
+
offense
|
|
1010
|
+
for offense in report["offenses"]
|
|
1011
|
+
if offense["check"] == "stylelint/block-no-empty"
|
|
1012
|
+
]
|
|
1013
|
+
|
|
1014
|
+
self.assertEqual(len(empty), 1)
|
|
1015
|
+
# start_row is zero-indexed, matching Theme Check.
|
|
1016
|
+
self.assertEqual(empty[0]["start_row"], expected_row)
|
|
1017
|
+
self.assertEqual(empty[0]["severity"], "error")
|
|
1018
|
+
# The masked interpolation is graded like any other value now: Liquid
|
|
1019
|
+
# does not belong in a `{% stylesheet %}` block, and Theme Check reports
|
|
1020
|
+
# it separately through `StaticStylesheetAndJavascriptTags`.
|
|
1021
|
+
interpolated_row = lines.index(" margin: {{ section.settings.gap }}px;")
|
|
1022
|
+
self.assertEqual(
|
|
1023
|
+
[
|
|
1024
|
+
offense["check"]
|
|
1025
|
+
for offense in report["offenses"]
|
|
1026
|
+
if offense["start_row"] == interpolated_row
|
|
1027
|
+
],
|
|
1028
|
+
["stylelint/declaration-property-value-no-unknown"],
|
|
1029
|
+
)
|
|
1030
|
+
|
|
1031
|
+
def test_grades_a_css_asset_by_the_two_rule_layers(self) -> None:
|
|
1032
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
1033
|
+
root = Path(directory)
|
|
1034
|
+
asset = root / "assets" / "component-td-card.css"
|
|
1035
|
+
asset.parent.mkdir(parents=True)
|
|
1036
|
+
asset.write_text(".td-card {\n margin: 0px;\n}\n.td-empty {}\n")
|
|
1037
|
+
|
|
1038
|
+
results = shopify_lint.run_css_lint(root, [asset])
|
|
1039
|
+
[report] = shopify_lint.normalize_stylelint_results(results)
|
|
1040
|
+
|
|
1041
|
+
severities = {
|
|
1042
|
+
offense["check"]: offense["severity"] for offense in report["offenses"]
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
self.assertEqual(severities["stylelint/block-no-empty"], "error")
|
|
1046
|
+
self.assertEqual(severities["stylelint/length-zero-no-unit"], "warning")
|
|
1047
|
+
|
|
1048
|
+
def test_reports_nothing_for_a_liquid_file_without_a_stylesheet_block(self) -> None:
|
|
1049
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
1050
|
+
root = Path(directory)
|
|
1051
|
+
section = root / "sections" / "td-plain.liquid"
|
|
1052
|
+
section.parent.mkdir(parents=True)
|
|
1053
|
+
section.write_text('<div class="td-plain">Hello</div>\n')
|
|
1054
|
+
|
|
1055
|
+
results = shopify_lint.run_css_lint(root, [section])
|
|
1056
|
+
|
|
1057
|
+
self.assertEqual(shopify_lint.normalize_stylelint_results(results), [])
|
|
1058
|
+
|
|
1059
|
+
|
|
585
1060
|
if __name__ == "__main__":
|
|
586
1061
|
unittest.main()
|
|
@@ -12,6 +12,13 @@ HardcodedText:
|
|
|
12
12
|
enabled: true
|
|
13
13
|
severity: warning
|
|
14
14
|
|
|
15
|
+
# Upstream check, pinned here so the branch lint always fails on it: Shopify
|
|
16
|
+
# serves `{% stylesheet %}` and `{% javascript %}` bodies verbatim, so Liquid
|
|
17
|
+
# written inside one ships to the browser as literal text.
|
|
18
|
+
StaticStylesheetAndJavascriptTags:
|
|
19
|
+
enabled: true
|
|
20
|
+
severity: error
|
|
21
|
+
|
|
15
22
|
UnusedSectionSettings:
|
|
16
23
|
enabled: false
|
|
17
24
|
severity: warning
|
|
@@ -112,6 +112,47 @@ All three nullability checks share the same branch and `default`-filter
|
|
|
112
112
|
analysis. A guard applies only inside its active `if`, `unless`, `elsif`, or
|
|
113
113
|
`case` branch and only to the matching setting, metafield, or metaobject.
|
|
114
114
|
|
|
115
|
+
## Left to upstream: Liquid in stylesheet and javascript tags
|
|
116
|
+
|
|
117
|
+
Shopify concatenates the contents of every `{% stylesheet %}` and
|
|
118
|
+
`{% javascript %}` block into the theme's compiled CSS and JavaScript bundles
|
|
119
|
+
**without rendering them**. Liquid written inside one is served to the browser
|
|
120
|
+
verbatim, so `color: {{ section.settings.color }}` ships as those thirty-six
|
|
121
|
+
characters and the declaration is dropped by the CSS parser. It looks like it
|
|
122
|
+
works locally only when the value happens to be absent.
|
|
123
|
+
|
|
124
|
+
Theme Check's own
|
|
125
|
+
[`StaticStylesheetAndJavascriptTags`](https://shopify.dev/docs/storefronts/themes/tools/theme-check/checks/static-stylesheet-and-javascript-tags)
|
|
126
|
+
already reports every Liquid node in those bodies — output tags, control flow,
|
|
127
|
+
and `{% comment %}`, which is not a comment there either — at `error` severity,
|
|
128
|
+
so this package deliberately ships no check of its own. The configuration below
|
|
129
|
+
pins it explicitly rather than inheriting it from `theme-check:recommended`, so
|
|
130
|
+
the branch lint keeps failing on it even if the upstream default changes.
|
|
131
|
+
|
|
132
|
+
Pass dynamic values in through the markup instead:
|
|
133
|
+
|
|
134
|
+
```liquid
|
|
135
|
+
<div class="td-hero" style="--td-hero-gap: {{ section.settings.gap }}px"></div>
|
|
136
|
+
{% stylesheet %}
|
|
137
|
+
.td-hero { gap: var(--td-hero-gap); }
|
|
138
|
+
{% endstylesheet %}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
For JavaScript, read the value from a `data-` attribute or a
|
|
142
|
+
`<script type="application/json">` block — both of which
|
|
143
|
+
`DisallowedScriptOrStyleTag` allows.
|
|
144
|
+
|
|
145
|
+
Its one blind spot is the mirror image: braces the Liquid parser reads as an
|
|
146
|
+
output tag when they are really host-language nesting, such as
|
|
147
|
+
`if (a) {{ b(); }}` or `@media screen {{ .a { color: red; } }}`, are reported as
|
|
148
|
+
Liquid. The masks in `eslint-plugin-theory` and `stylelint-config-theory` treat
|
|
149
|
+
anything between `{{` and `}}` carrying a brace or a semicolon as CSS or
|
|
150
|
+
JavaScript, so they keep linting such a block. If a theme genuinely needs the
|
|
151
|
+
construct, put a
|
|
152
|
+
`{% # theme-check-disable StaticStylesheetAndJavascriptTags %}` comment *before*
|
|
153
|
+
the opening tag — never inside the block, where the comment would itself be
|
|
154
|
+
emitted verbatim.
|
|
155
|
+
|
|
115
156
|
## Usage in a project
|
|
116
157
|
|
|
117
158
|
Install the module as a development dependency:
|
|
@@ -137,6 +178,13 @@ HardcodedText:
|
|
|
137
178
|
enabled: true
|
|
138
179
|
severity: warning
|
|
139
180
|
|
|
181
|
+
# Upstream check, pinned here so the branch lint always fails on it: Shopify
|
|
182
|
+
# serves `{% stylesheet %}` and `{% javascript %}` bodies verbatim, so Liquid
|
|
183
|
+
# written inside one ships to the browser as literal text.
|
|
184
|
+
StaticStylesheetAndJavascriptTags:
|
|
185
|
+
enabled: true
|
|
186
|
+
severity: error
|
|
187
|
+
|
|
140
188
|
UnusedSectionSettings:
|
|
141
189
|
enabled: false
|
|
142
190
|
severity: warning
|