Repository: plone/cookieplone

Issue:
Implement filter to convert a NPM package name with organization into the package path

Discussion:

Solution patch:
diff --git a/cookieplone/filters/__init__.py b/cookieplone/filters/__init__.py
index 9735565..068b2e1 100644
--- a/cookieplone/filters/__init__.py
+++ b/cookieplone/filters/__init__.py
@@ -6,7 +6,7 @@
 from cookiecutter.utils import simple_filter
 
 from cookieplone.cli import parse_boolean
-from cookieplone.utils import containers, versions
+from cookieplone.utils import containers, npm, versions
 
 
 @simple_filter
@@ -133,3 +133,9 @@ def image_prefix(registry: str) -> str:
 def as_semver(v: str) -> str:
     """Convert a PEP 440 version to SemVer."""
     return versions.convert_pep440_semver(v)
+
+
+@simple_filter
+def unscoped_package_name(v: str) -> str:
+    """Return the unscoped name for a given package."""
+    return npm.unscoped_package_name(v)
diff --git a/cookieplone/utils/npm.py b/cookieplone/utils/npm.py
new file mode 100644
index 0000000..5fd7b9d
--- /dev/null
+++ b/cookieplone/utils/npm.py
@@ -0,0 +1,18 @@
+def _is_scoped_package(name: str) -> bool:
+    """Check if package is scoped."""
+    return name.startswith("@") and "/" in name
+
+
+def parse_package_name(name: str) -> tuple[str, str]:
+    """Parses an npm name, and returns organization and package_name."""
+    org = ""
+    package_name = name
+    if _is_scoped_package(name):
+        org, package_name = name.split("/", 1)
+    return org, package_name
+
+
+def unscoped_package_name(name: str) -> str:
+    """Return the unscoped package name for an npm package."""
+    _, package_name = parse_package_name(name)
+    return package_name
diff --git a/news/74.feature b/news/74.feature
new file mode 100644
index 0000000..cfc008f
--- /dev/null
+++ b/news/74.feature
@@ -0,0 +1 @@
+Add filter `unscoped_package_name` to return the npm package name without its scope. @ericof


Test patch:
diff --git a/tests/test_filters.py b/tests/test_filters.py
index db2fad7..b1114ce 100644
--- a/tests/test_filters.py
+++ b/tests/test_filters.py
@@ -59,7 +59,21 @@ def func(filter_: str) -> Path:
         ["python_version_latest", "{{'6.1' | python_version_latest}}", "3.13"],
         ["as_semver", "{{'6.1' | as_semver}}", "6.1.0"],
         ["as_semver", "{{'1.0.0a0' | as_semver}}", "1.0.0-alpha.0"],
-        ["as_semver", "{{'1.0.0a0.dev0' | as_semver}}", "1.0.0-alpha.0+dev-0"],
+        [
+            "unscoped_package_name",
+            "{{'volto-addon' | unscoped_package_name}}",
+            "volto-addon",
+        ],
+        [
+            "unscoped_package_name",
+            "{{'@plone-collective/volto-addon' | unscoped_package_name}}",
+            "volto-addon",
+        ],
+        [
+            "unscoped_package_name",
+            "{{'@plone/volto' | unscoped_package_name}}",
+            "volto",
+        ],
     ],
 )
 def test_filters(generate_context_file, filter_: str, raw: str, expected: str):
diff --git a/tests/utils/test_npm.py b/tests/utils/test_npm.py
new file mode 100644
index 0000000..45c319a
--- /dev/null
+++ b/tests/utils/test_npm.py
@@ -0,0 +1,42 @@
+import pytest
+
+from cookieplone.utils import npm
+
+
+@pytest.mark.parametrize(
+    "name,expected",
+    [
+        ("volto-addon", False),
+        ("@plone-collective/volto-addon", True),
+        ("@plone/volto", True),
+    ],
+)
+def test__is_scoped_package(name: str, expected: bool):
+    func = npm._is_scoped_package
+    assert func(name) is expected
+
+
+@pytest.mark.parametrize(
+    "name,expected",
+    [
+        ("volto-addon", ("", "volto-addon")),
+        ("@plone-collective/volto-addon", ("@plone-collective", "volto-addon")),
+        ("@plone/volto", ("@plone", "volto")),
+    ],
+)
+def test_parse_package_name(name: str, expected: tuple[str, str]):
+    func = npm.parse_package_name
+    assert func(name) == expected
+
+
+@pytest.mark.parametrize(
+    "name,expected",
+    [
+        ("volto-addon", "volto-addon"),
+        ("@plone-collective/volto-addon", "volto-addon"),
+        ("@plone/volto", "volto"),
+    ],
+)
+def test_unscoped_package_name(name: str, expected: str):
+    func = npm.unscoped_package_name
+    assert func(name) == expected